在本教程中,我们将学习如何构建一个网站,用于在区块链 Flow 上收集数字收藏品(或 NFT)。我们将使用智能合约语言 Cadence 和 React 来实现这一切。我们还将了解 Flow、它的优点以及我们可以使用的有趣工具。
读完本文后,您将拥有在 Flow 区块链上创建自己的去中心化应用程序所需的工具和知识。
让我们开始吧!
我们正在构建一个数字收藏品应用程序。每个收藏品都是不可替代的代币(NFT)。
为了使这一切顺利进行,我们将使用 Flow 的 NonFungibleToken 标准,这是一组帮助我们管理这些特殊数字项目的规则(类似于以太坊中的 ERC-721)。
开始之前,请务必在您的系统上安装 Flow CLI。如果您还没有这样做,请按照这些安装说明进行操作。
如果您准备好启动项目,请首先输入命令流设置。
该命令在幕后发挥了一些作用,为您的项目奠定了基础。它创建一个文件夹系统并设置一个名为 flow.json 的文件来配置您的项目,确保一切都井井有条并准备就绪!
该项目将包含cadence
文件夹和flow.json
文件。 (flow.json 文件是项目的配置文件,自动维护。)
Cadence 文件夹包含以下内容:
请按照以下步骤使用 Flow NFT 标准。
首先,转到flow-collectibles-portal
文件夹,找到cadence
文件夹。然后,打开contracts
文件夹。创建一个新文件,并将其命名为NonFungibleToken.cdc
。
现在,打开名为NonFungibleToken的链接,其中包含 NFT 标准。复制该文件中的所有内容,并将其粘贴到您刚刚创建的新文件(“NonFungibleToken.cdc”)中。
就是这样!您已经成功地为您的项目设置了标准。
现在,让我们编写一些代码!
然而,在我们深入编码之前,开发人员建立如何构建代码的心理模型非常重要。
在顶层,我们的代码库由三个主要组件组成:
NFT:每个收藏品都表示为 NFT。
集合:集合是指特定用户拥有的一组 NFT。
全局函数和变量:这些是在智能合约的全局级别定义的函数和变量,不与任何特定资源关联。
在cadence/contracts
中创建一个名为Collectibles.cdc
的新文件。这是我们将编写代码的地方。
合约结构
import NonFungibleToken from "./NonFungibleToken.cdc" pub contract Collectibles: NonFungibleToken{ pub var totalSupply: UInt64 // other code will come here init(){ self.totalSupply = 0 } }
让我们逐行分解代码:
首先,我们需要通过包含所谓的“NonFungibleToken”来标准化我们正在构建的 NFT。这是由 Flow 构建的 NFT 标准,定义了每个 NFT 智能合约必须包含的以下功能集。
导入后,让我们创建合同。为此,我们使用pub contract [contract name]
。每次创建新合约时都使用相同的语法。您可以用您想要的合同名称填写contract name
。在我们的例子中,我们将其称为Collectibles
。
接下来,我们要确保我们的合约遵循 NonFungibleToken 的一组特定功能和规则。为此,我们在“:”的帮助下添加一个 NonFungibleToken 接口。
像这样( `pub contract Collectibles: NonFungibleToken{}`
)
每个合约都必须具有init()
函数。它在合约最初部署时被调用。这类似于 Solidity 所说的构造函数。
现在,我们创建一个名为totalSupply
数据类型为UInt64
全局变量。该变量将跟踪您的收藏品总数。
现在,用值0
初始化totalSupply
。
就是这样!我们为Collectibles
合同奠定了基础。现在,我们可以开始添加更多特性和功能,使其更加令人兴奋。
在继续之前,请查看代码片段以了解我们如何在 Cadence 中定义变量:
将以下代码添加到您的智能合约中:
import NonFungibleToken from "./NonFungibleToken.cdc" pub contract Collectibles: NonFungibleToken{ // above code… pub resource NFT: NonFungibleToken.INFT{ pub let id: UInt64 pub var name: String pub var image: String init(_id:UInt64, _name:String, _image:String){ self.id = _id self.name = _name self.image = _image } } // init()... }
正如您之前所看到的,该合约实现了 NFT 标准接口,以pub contract Collectibles: NonFungibleToken
为代表。同样,资源也可以实现各种资源接口。
因此,让我们将NonFungibleToken.INFT
接口添加到 NFT 资源中,该接口要求资源中存在一个名为 id 的公共属性。
以下是我们将在 NFT 资源中使用的变量:
id:
维护NFT的IDname:
NFT 的名称。image:
NFT 的图片 URL。
定义变量后,请务必在init()
函数中初始化该变量。
让我们继续创建另一个名为Collection Resource
的资源。
首先,您需要了解Collection Resources
工作原理。
如果您需要在笔记本电脑上存储音乐文件和几张照片,您会怎么做?
通常,您会导航到本地驱动器(假设您的 D 驱动器)并创建music
文件夹和photos
文件夹。然后,您可以将音乐和照片文件复制并粘贴到目标文件夹中。
同样,这就是 Flow 上的数字收藏品的工作原理。
想象一下,您的笔记本电脑是Flow Blockchain Account
,您的 D 驱动器是Account Storage
,文件夹是Collection
。
因此,当与任何项目交互购买 NFT 时,该项目会在您的account storage
中创建其collection
,类似于在 D 驱动器上创建文件夹。当您与 10 个不同的 NFT 项目交互时,您的帐户中最终会出现 10 个不同的集合。
这就像拥有一个个人空间来存储和整理您独特的数字宝藏!
import NonFungibleToken from "./NonFungibleToken.cdc" pub contract Collectibles: NonFungibleToken{ //Above code NFT Resource… // Collection Resource pub resource Collection{ } // Below code… }
每个collection
都有一个ownedNFTs
变量来保存NFT Resources
。
pub resource Collection { pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} init(){ self.ownedNFTs <- {} } }
资源接口
Flow 中的resource
接口与其他编程语言中的接口类似。它位于资源之上,并确保实现它的资源具有接口定义的所需功能。
它还可用于限制对整个资源的访问,并且在访问修饰符方面比资源本身更具限制性。
在NonFungibleToken
标准中,有几个资源接口,例如INFT
、 Provider
、 Receiver
和CollectionPublic
。
每个接口都有特定的功能和字段,需要由使用它们的资源来实现。
在此合约中,我们将使用NonFungibleToken: Provider
、 Receiver
和CollectionPublic
。这些接口定义了deposit
、 withdraw
、 borrowNFT
和getIDs
功能。我们将详细解释其中每一个。
我们还将添加一些从这些函数发出的事件,并声明一些我们将在本教程中进一步使用的变量。
pub contract Collectibles:NonFungibleToken{ // rest of the code… pub event ContractInitialized() pub event Withdraw(id: UInt64, from: Address?) pub event Deposit(id: UInt64, to: Address?) pub let CollectionStoragePath: StoragePath pub let CollectionPublicPath: PublicPath pub resource interface CollectionPublic{ pub fun deposit(token: @NonFungibleToken.NFT) pub fun getIDs(): [UInt64] pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT } pub resource Collection: CollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic{ pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} init(){ self.ownedNFTs <- {} } } }
提取
现在,让我们创建该接口所需的withdraw()
函数。
pub resource Collection: CollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic{ // other code 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 } init()... }
借助该功能,您可以将 NFT 资源移出集合。如果它:
然后,调用者可以使用该资源并将其保存在其帐户存储中。
订金
现在,是时候使用NonFungibleToken.Receiver
所需的deposit()
函数了。
pub resource Collection: CollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic{ // other code 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 } pub fun deposit(token: @NonFungibleToken.NFT) { let id = token.id let oldToken <- self.ownedNFTs[id] <-token destroy oldToken emit Deposit(id: id, to: self.owner?.address) } init()... }
借用并获取ID
现在,让我们关注NonFungibleToken.CollectionPublic: borrowNFT()
和getID()
。
pub resource Collection: CollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic{ // other code 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 } pub fun deposit(token: @NonFungibleToken.NFT) { let id = token.id let oldToken <- self.ownedNFTs[id] <-token destroy oldToken emit Deposit(id: id, to: self.owner?.address) } pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { if self.ownedNFTs[id] != nil { return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)! } panic("NFT not found in collection.") } pub fun getIDs(): [UInt64]{ return self.ownedNFTs.keys } init()... }
析构函数
对于集合资源,我们最不需要的就是析构函数。
destroy (){ destroy self.ownedNFTs }
由于Collection资源包含其他资源(NFT资源),因此我们需要指定一个析构函数。当对象被销毁时,析构函数就会运行。这确保了资源在其父资源被破坏时不会“无家可归”。我们不需要 NFT 资源的析构函数,因为它不包含任何其他资源。
我们看一下完整的集合资源源码:
import NonFungibleToken from "./NonFungibleToken.cdc" pub contract Collectibles: NonFungibleToken{ pub var totalSupply: UInt64 pub resource NFT: NonFungibleToken.INFT{ pub let id: UInt64 pub var name: String pub var image: String init(_id:UInt64, _name:String, _image:String){ self.id = _id self.name = _name self.image = _image } } pub resource interface CollectionPublic{ pub fun deposit(token: @NonFungibleToken.NFT) pub fun getIDs(): [UInt64] pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT } pub event ContractInitialized() pub event Withdraw(id: UInt64, from: Address?) pub event Deposit(id: UInt64, to: Address?) pub let CollectionStoragePath: StoragePath pub let CollectionPublicPath: PublicPath pub resource Collection: CollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic{ pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} init(){ self.ownedNFTs <- {} } destroy (){ destroy self.ownedNFTs } 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 } pub fun deposit(token: @NonFungibleToken.NFT) { let id = token.id let oldToken <- self.ownedNFTs[id] <-token destroy oldToken emit Deposit(id: id, to: self.owner?.address) } pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { if self.ownedNFTs[id] != nil { return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)! } panic("NFT not found in collection.") } pub fun getIDs(): [UInt64]{ return self.ownedNFTs.keys } } init(){ self.CollectionPublicPath = /public/NFTCollection self.CollectionStoragePath = /storage/NFTCollection self.totalSupply = 0 emit ContractInitialized() } }
现在,我们已经完成了所有资源。接下来,我们将看看全局函数。
全局函数是在智能合约的全局级别上定义的函数,这意味着它们不属于任何资源。这些可供公众访问和调用,并向公众公开智能合约的核心功能。
createEmptyCollection :此函数将一个空的Collectibles.Collection
初始化到调用者帐户存储中。
checkCollection :这个方便的功能可以帮助您发现您的帐户是否已经有collection
资源。
mintNFT :这个功能非常酷,因为它允许任何人创建 NFT。
// pub resource Collection… pub fun createEmptyCollection(): @Collection{ return <- create Collection() } pub fun checkCollection(_addr: Address): Bool{ return getAccount(_addr) .capabilities.get<&{Collectibles.CollectionPublic}> (Collectibles.CollectionPublicPath)! .check() } pub fun mintNFT(name:String, image:String): @NFT{ Collectibles.totalSupply = Collectibles.totalSupply + 1 let nftId = Collectibles.totalSupply var newNFT <- create NFT(_id:nftId, _name:name, _image:image) return <- newNFT } init()...
现在,一切就绪后,我们终于完成了智能合约的编写。在这里查看最终代码。
现在,让我们看看用户如何与部署在 Flow 区块链上的智能合约进行交互。
与 Flow 区块链交互有两个步骤:
交易是经过加密签名的数据,其中包含一组与智能合约交互以更新 Flow 状态的指令。简单来说,这就像一个改变区块链上数据的函数调用。交易通常会涉及一些成本,该成本可能会根据您所在的区块链而有所不同。
事务包括多个可选阶段: prepare
、 pre
、 execute
阶段和post
阶段。
您可以在有关事务的 Cadence 参考文档中阅读更多相关内容。每个阶段都有一个目的;最重要的两个阶段是prepare
和execute
。
Prepare Phase
:此阶段用于访问签名者帐户内的数据和信息(AuthAccount 类型允许)。
Execute Phase
:此阶段用于执行操作。
现在,让我们为我们的项目创建一个交易。
按照以下步骤在项目文件夹中创建事务。
首先,转到项目文件夹并打开cadence
文件夹。在其中,打开transaction
文件夹,并创建一个名为Create_Collection.cdc
和mint_nft.cdc
的新文件
import Collectibles from "../contracts/Collectibles.cdc" transaction { prepare(signer: AuthAccount) { if signer.borrow<&Collectibles.Collection>(from: Collectibles.CollectionStoragePath) == nil { let collection <- Collectibles.createEmptyCollection() signer.save(<-collection, to: Collectibles.CollectionStoragePath) let cap = signer.capabilities.storage.issue<&{Collectibles.CollectionPublic}>(Collectibles.CollectionStoragePath) signer.capabilities.publish( cap, at: Collectibles.CollectionPublicPath) } } }
让我们逐行分解这段代码:
该交易与收藏品智能合约交互。然后,它通过从指定存储路径Collectibles.CollectionStoragePath
借用对 Collection 资源的引用来检查发送者(签名者)的帐户中是否存储有 Collection 资源。如果引用为零,则意味着签名者还没有集合。
如果签名者没有集合,则会通过调用createEmptyCollection()
函数创建一个空集合。
创建空集合后,将其放入签名者帐户中指定的存储路径Collectibles.CollectionStoragePath
下。
这将使用link()
在签名者的帐户和新创建的集合之间建立链接。
import NonFungibleToken from "../contracts/NonFungibleToken.cdc" import Collectibles from "../contracts/Collectibles.cdc" transaction(name:String, image:String){ let receiverCollectionRef: &{NonFungibleToken.CollectionPublic} prepare(signer:AuthAccount){ self.receiverCollectionRef = signer.borrow<&Collectibles.Collection>(from: Collectibles.CollectionStoragePath) ?? panic("could not borrow Collection reference") } execute{ let nft <- Collectibles.mintNFT(name:name, image:image) self.receiverCollectionRef.deposit(token: <-nft) } }
让我们逐行分解这段代码:
我们首先导入NonFungibleToken
和Collectibles contract
。
transaction(name: String, image: String)
此行定义了一个新事务。它有两个参数,名称和图像,都是字符串类型。这些参数用于传递正在铸造的 NFT 的名称和图像。
let receiverCollectionRef: &{NonFungibleToken.CollectionPublic}
此行声明一个新变量receiverCollectionRef.
它是对NonFungibleToken.CollectionPublic
类型的 NFT 公共集合的引用。该参考将用于与我们将存放新铸造的 NFT 的集合进行交互。
prepare(signer: AuthAccount)
此行启动准备块,该块在事务之前执行。它需要一个类型为AuthAccount
的参数签名者。 AuthAccount
代表交易签名者的帐户。
它从准备块内的签名者存储中借用了对Collectibles.Collection
的引用。它使用借用函数来访问集合的引用并将其存储在receiverCollectionRef
变量中。
如果找不到引用(例如,如果签名者的存储中不存在集合),则会抛出错误消息“无法借用集合引用”。
execute
块包含事务的主要执行逻辑。该块内的代码将在prepare
块成功完成后执行。
nft <- Collectibles.mintNFT(_name: name, image: image)
在execute
块内,此行使用提供的名称和图像参数从Collectibles
合约中调用mintNFT
函数。该函数预计将创建一个具有给定名称和图像的新 NFT。 <-
符号表示 NFT 正在作为可移动的对象(资源)被接收。
self.receiverCollectionRef.deposit(token: <-nft)
此行将新铸造的 NFT 存入指定集合中。它使用receiverCollectionRef
上的存款函数将NFT的所有权从交易的执行帐户转移到集合。这里的<-
符号也表示NFT在deposit
过程中作为资源被转移。
我们使用脚本来查看或读取区块链中的数据。脚本是免费的,不需要签名。
按照以下步骤在项目文件夹中创建脚本。
首先,转到项目文件夹并打开cadence
文件夹。在其中打开script
文件夹,并创建一个名为view_nft.cdc
的新文件。
import NonFungibleToken from "../contracts/NonFungibleToken.cdc" import Collectibles from "../contracts/Collectibles.cdc" pub fun main(user: Address, id: UInt64): &NonFungibleToken.NFT? { let collectionCap= getAccount(user).capabilities .get<&{Collectibles.CollectionPublic}>(/public/NFTCollection) ?? panic("This public capability does not exist.") let collectionRef = collectionCap.borrow()! return collectionRef.borrowNFT(id: id) }
让我们逐行分解这段代码:
首先,我们导入NonFungibleToken
和Collectibles
合约。
pub fun main(acctAddress: Address, id: UInt64): &NonFungibleToken.NFT?
这一行定义了脚本的入口点,它是一个名为 main 的公共函数。该函数有两个参数:
acctAddress
: Address
类型参数,表示 Flow 区块链上帐户的地址。
id
: UInt64
类型参数,表示集合中 NFT 的唯一标识符。
然后我们使用getCapability
获取指定acctAddress
的Collectibles.Collection
功能。能力是对允许访问其功能和数据的资源的引用。在本例中,它正在获取Collectibles.Collection
资源类型的功能。
然后,我们使用borrowNFT
函数从collectionRef
中借用NFT。 borrowNFT
函数采用id
参数,该参数是集合中 NFT 的唯一标识符。功能上的borrow
功能允许读取资源数据。
最后,我们从函数中返回 NFT。
现在,是时候将我们的智能合约部署到 Flow 测试网了。
1. 设置 Flow 账户。
在终端中运行以下命令生成 Flow 帐户:
flow keys generate
请务必记下您的公钥和私钥。
接下来,我们将前往
将您的公钥粘贴到指定的输入字段中。
将签名和哈希算法设置为默认值。
完成验证码。
单击创建帐户。
设置帐户后,我们会收到与新 Flow 地址的对话,其中包含 1,000 个测试 Flow 代币。复制该地址,以便我们以后可以使用它。
2. 配置项目。
现在,让我们配置我们的项目。最初,当我们设置项目时,它创建了一个flow.json
文件。
这是 Flow CLI 的配置文件,定义 Flow CLI 可以为您执行的操作的配置。可以将其视为大致相当于以太坊上的hardhat.config.js
。
现在,打开代码编辑器,将以下代码复制并粘贴到flow.json
文件中。
{ "contracts": { "Collectibles": "./cadence/contracts/Collectibles.cdc", "NonFungibleToken": { "source": "./cadence/contracts/NonFungibleToken.cdc", "aliases": { "testnet": "0x631e88ae7f1d7c20" } } }, "networks": { "testnet": "access.devnet.nodes.onflow.org:9000" }, "accounts": { "testnet-account": { "address": "ENTER YOUR ADDRESS FROM FAUCET HERE", "key": "ENTER YOUR GENERATED PRIVATE KEY HERE" } }, "deployments": { "testnet": { "testnet-account": [ "Collectibles" ] } } }
将生成的私钥粘贴到代码中的位置(密钥:“在此处输入您生成的私钥”)。
现在,在测试网上执行代码。转到终端,然后运行以下代码:
flow project deploy --network testnet
5. 等待确认。
提交交易后,您将收到交易 ID。等待交易在测试网上得到确认,表明智能合约已成功部署。
在此处检查您部署的合同。
在GitHub上查看完整代码。
恭喜!您现在已经在 Flow 区块链上构建了一个收藏品门户并将其部署到测试网上。下一步是什么?现在,您可以构建我们将在本系列的第 2 部分中介绍的前端。
祝你有美好的一天!
也发布在这里