Flow区块链NFT开发教程

Flow区块链是著名的加密猫团队特别针对NFT应用开发的新的公链, 其目的是解决以太坊在NFT开发应用上存在的诸多问题,目前已经得到 NBA、UFC等多个大牌厂商的支持。在这个教程中我们将学习如何 创建Cadence智能合约并在Flow区块链上发行NFT。

区块链开发教程链接: 以太坊 | 比特币 | EOS | Tendermint | Hyperledger Fabric | Omni/USDT | Ripple | Tron

1、Flow开发环境安装

首先我们需要安装Flow CLI,详细的安装说明可以查看Flow官方文档, 在这里我们仅列出几种操作系统下的安装命令:

  • macOS
1
brew install flow-cli
  • Linux
1
sh -ci “$(curl -fsSL https://storage.googleapis.com/flow-cli/install.sh)"
  • Windows
1
iex “& { $(irm ‘https://storage.googleapis.com/flow-cli/install.ps1') }”

我们需要将文件保存到IPFS,为此我们使用Pinata。你可以 注册一个免费账号,然后点击这里获取API访问密钥。 在下一教程中我们将使用Pinata API。

我们还需要安装NodeJS和一个支持Flow智能合约代码语法高亮的文本编辑器,Flow的 智能合约使用Cadence语言编写。Visual Studio Code提供Cadence高亮扩展

下面让我们创建项目目录:

1
mkdir pinata-party

进入该目录,然后执行如下命令初始化一个新的Flow项目:

1
2
cd pinata-party
flow project init

现在使用你喜欢的代码编辑器(比如加载了Cadence扩展的Visual Studio Code)打开项目文件。

可以看到flow.json,我们接下来就会用到。首先创建一个cadence文件夹,并在该文件夹内创建 contracts文件夹,最后在contracts文件夹内创建合约文件PinataPartyContract.cdc。

在我们继续之前,需要指出的是,下面我们的操作将在Flow区块链仿真器上完成,要将项目 部署到测试链或主链,只需要更新flow.json中的配置参数即可。现在让我们在flow.json中 设置仿真环境以便接下来进行智能合约代码的编写:

1
2
3
"contracts": {
"PinataPartyContract": "./cadence/contracts/PinataPartyContract.cdc"
}

接下来更新flow.json中的deployments对象:

1
2
3
4
5
"deployments": {
"emulator": {
"emulator-account": ["PinataPartyContract"]
}
}

上面的设置用来告诉Flow CLI使用仿真器来部署合约,在配置中同时也声明了账号以及我们 接下来要开发的合约。

2、编写Cadence智能合约

Flow提供了一个创建NFT智能合约的出色的教程,这是一个很好的参考,但是Flow自己同时 指出,他们还没有解决NFT元数据的问题。Flow团队希望在链上存储元数据。这是一个好主意, 他们肯定会给出一个合理的解决方案。然而我们现在就希望铸造一些带元数据的通证,我们也 希望这些元数据关联到NFT。元数据只是一个部分,我们也希望指向通证最终代表的媒体。

如果你熟悉以太坊区块链上的NFT,你可能知道这些NFT支持的资产存储在传统数据库和云主机上。 在大多数情况下这没太大问题。我们之前讨论过可寻址内容的天才思路,以及在传统云平台上 存储区块链附加数据的问题,这指向了两个要点:

  • 资产应当可验证
  • 应当易于转移维护责任

IPFS解决了这些问题,Pinata则提供了一个简单的接入层方面我们将内容长期锁定在IPFS上。 这就是我们希望的解决方案,对吗?我们希望确保可以证明NFT的所有权,提供NFT的数据, 并确保我们可以控制IPFS上的底层资产。

综合这些想法,现在我们来编写智能合约,该合约将负责铸造NFT、关联元数据并确保元数据 指向保存在IPFS上的底层资产。

打开PinataPartyContract.cdc文件,输入如下代码:

1
2
3
4
5
6
7
8
pub contract PinataPartyContract {
pub resource NFT {
pub let id: UInt64
init(initID: UInt64) {
self.id = initID
}
}
}

第一步是定义我们的合约,并在合约内创建一个资源。资源存储在用户账号中,对其的 访问需要满足访问控制条件。NFT必须是唯一标识的,id属性就是用来标识我们的通证。

接下来我们需要创建一个资源接口以便定义可以为其他人可用的能力,例如合约主之外的 其他人。

1
2
3
4
5
6
pub resource interface NFTReceiver {
pub fun deposit(token: @NFT, metadata: {String : String})
pub fun getIDs(): [UInt64]
pub fun idExists(id: UInt64): Bool
pub fun getMetadata(id: UInt64) : {String : String}
}

将上述代码放在NFT资源代码下面。这个NFTReceiver接口定义了具备访问权限的人可以调用 以下方法:

  • deposit
  • getIDs
  • idExists
  • getMetadata

接下来我们需要实现通证集,可以将其视为保存用户全部的NFT的钱包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
pub resource Collection: NFTReceiver {
pub var ownedNFTs: @{UInt64: NFT}
pub var metadataObjs: {UInt64: { String : String }}

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

pub fun withdraw(withdrawID: UInt64): @NFT {
let token <- self.ownedNFTs.remove(key: withdrawID)!

return <-token
}

pub fun deposit(token: @NFT, metadata: {String : String}) {
self.metadataObjs[token.id] = metadata
self.ownedNFTs[token.id] <-! token
}

pub fun idExists(id: UInt64): Bool {
return self.ownedNFTs[id] != nil
}

pub fun getIDs(): [UInt64] {
return self.ownedNFTs.keys
}

pub fun updateMetadata(id: UInt64, metadata: {String: String}) {
self.metadataObjs[id] = metadata
}

pub fun getMetadata(id: UInt64): {String : String} {
return self.metadataObjs[id]!
}

destroy() {
destroy self.ownedNFTs
}
}

上面代码中包含了很多内容,不过我们将很快理解其中的含义。

首先,我们有一个名为onwedNFTs的变量,它负责跟踪一个用户持有的全部NFT。

接下来,名为metadataObjs的变量用来保存每个NFT的元数据映射,这个变量 将通证id映射到关联的元数据,因此在设置之前我们需要先有通证id。

然后我们初始化变量。在Flow的资源中定义的变量需要进行初始化。

最后我们实现NFT集资源的全部函数。注意这些函数并非都向外部公开。别忘了 之前我们在NFTReceiver资源接口中定义了任何人可以访问的函数。

我还想特别指出deposit函数。因为我们继承了默认的Flow NFT合约以便包含 metadataObjs映射,我们也扩展默认的deposit函数来接收额外的metadata参数。 为什么要这么做?我们需要确保只有通证的铸造人可以添加通证的元数据。 为保持其私有化,我们将元数据的初始添加限制在铸造环节。

快要完成Cadence合约代码了,在NFT集资源下面,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub fun createEmptyCollection(): @Collection {
return <- create Collection()
}

pub resource NFTMinter {
pub var idCount: UInt64

init() {
self.idCount = 1
}

pub fun mintNFT(): @NFT {
var newNFT <- create NFT(initID: self.idCount)

self.idCount = self.idCount + 1 as UInt64

return <-newNFT
}
}

首先,我们定义一个函数负责创建空的NFT集合。这是初次访问我们合约的 用户获得存储位置的实现代码。

之后,我们再创建一个资源,这很重要,否则将无法铸造通证。NFTMinter资源 包含递增的idCount以确保我们的NFT的id不会重复。代码还定义了一个创建NFT 的函数。

在NFTMinter资源下面,添加主合约初始化代码:

1
2
3
4
5
init() {
self.account.save(<-self.createEmptyCollection(), to: /storage/NFTCollection)
self.account.link<&{NFTReceiver}>(/public/NFTReceiver, target: /storage/NFTCollection)
self.account.save(<-create NFTMinter(), to: /storage/NFTMinter)
}

这个初始化函数仅在合约部署时调用。它主要完成以下工作:

  1. 创建一个空的NFT集合,以便合约主可以铸造并持有NFT
  2. 集合资源发布到引用NFTReceiver接口的公共位置,这表明了NFTReceiver定义的方法可以被外部访问
  3. NFTMinter资源保存在合约创建者的账号存储中,这意味着只有合约创建者可以铸造通证

可以在这里查看完整的合约代码。

3、在Flow区块链部署Cadence合约

现在我们有了合约,在部署之前,最好使用Flow Playground进行测试。 前往Flow Playground并打开侧栏的第一个账号,用我们的合约代码替换示例合约,然后点击Deploy。 如果一切顺利,你应该在日志窗口看到类似下面这样的内容:

1
16:48:55 Deployment Deployed Contract To: 0x01

现在我们准备将合约部署到本地运行的仿真器上了。在命令行运行如下命令:

1
flow project start-emulator

仿真器运行并且flow.json配置正确的话,我们可以执行如下命令部署合约:

1
flow project deploy

如果一切顺利的话,应当可以看到下面这样的输出:

1
2
Deploying 1 contracts for accounts: emulator-account
PinataPartyContract -> 0xf8d6e0586b0a20c7

现在我们在Flow仿真器上有了一个激活的合约,接下来该铸造NFT了。

4、使用Cadence脚本铸造NFT

在下一个教程中,我们将创建一个app和用户界面来让整个铸造过程更加用户友好。 不过在这个教程中,为了说明元数据与NFT在Flow上如何运作,我们将使用Cadence脚本和命令行 来铸造NFT。

让我们在项目目录下创建一个新的目录transactions,然后在该目录中创建一个文件 MintPinataParty.cdc。

为了编写交易,我们需要使用一个文件来引用提供给NFT的元数据。为此我们将通过Pinata 上传文件到IPFS。点击这里上传你选好的视频文件。

上传文件后你会得到一个IPFS哈希(通常被称为内容标识符或CID)。拷贝下来这个 哈希,因为我们在铸造过程中需要用到它。

现在,在你的MintPinataParty.cdc文件中,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import PinataPartyContract from 0xf8d6e0586b0a20c7

transaction {
let receiverRef: &{PinataPartyContract.NFTReceiver}
let minterRef: &PinataPartyContract.NFTMinter

prepare(acct: AuthAccount) {
self.receiverRef = acct.getCapability<&{PinataPartyContract.NFTReceiver}>(/public/NFTReceiver)
.borrow()
?? panic("Could not borrow receiver reference")

self.minterRef = acct.borrow<&PinataPartyContract.NFTMinter>(from: /storage/NFTMinter)
?? panic("could not borrow minter reference")
}

execute {
let metadata : {String : String} = {
"name": "The Big Swing",
"swing_velocity": "29",
"swing_angle": "45",
"rating": "5",
"uri": "ipfs://QmRZdc3mAMXpv6Akz9Ekp1y4vDSjazTx2dCQRkxVy1yUj6"
}
let newNFT <- self.minterRef.mintNFT()

self.receiverRef.deposit(token: <-newNFT, metadata: metadata)

log("NFT Minted and deposited to Account 2's Collection")
}
}

这个交易很简单,让我们解释一下。

首先你会注意到开头的import语句。你可能还记得我们部署合约时收到一个账号,这就是我们 需要引用的。因此将0xf8d6e0586b0a20c7替换为部署的账号地址。

接下来我们定义交易。这里的内容与我们期望执行的交易有关。

在我们的交易中做的第一件事,就是定义两个引用变量receiverRef和minterRef。在这个案例 中我们同时是NFT的接收方和铸造方。这两个变量引用我们在合约中创建的资源。如果执行 交易的人没有访问资源的权限,交易将失败。

接下来我们定义了一个prepare函数。这个函数输入账号信息并进行一些验证。我们尝试借用 在NFTMinter和NFTReceiver上定义的能力,如果执行人没有权限,就会在这里失败。

最后是我们的execute函数。这个函数中我们为NFT构造元数据,铸造NFT,关联元数据,将NFT 存入我们的账号。可以看到我创建了一个metadata变量,在其中添加了关于我们通证的信息。 由于我们希望尽可能复制NBA Top Shot,在元数据中我还定义了一些统计信息,当然你可以 输入任何有意义的信息。

你会注意到,在metadata中我也定义了一个uri属性,这指向保存我们资产文件的IPFS哈希。 在这个示例中就是我们上传的视频,你可以将其替换为你得到的哈希。

出于一些考虑我们在哈希之前附加ipfs://前缀,这样在IPFS桌面客户端和浏览器扩展中就可以 访问,也可以在Brave浏览器中直接访问,Brave提供了IPFS的原生支持。

我们调用mintNFT函数,它负责创建通证。接下来需要调用deposit函数以便将其放入账号。

最后是简单的日志输出。

现在我们基本上准备好发送交易、铸造NFT了。但是首先我们需要准备账号。在命令行进入 项目根目录,创建一个新的私钥用于签名。

1
flow keys generate

上面的命令会生成公钥和私钥。记得保护好你的私钥!

我们需要私钥来签名交易,因此将其拷贝到flow.json文件中。我们还需要指定签名算法。 最后看起来是这样:

1
2
3
4
5
6
7
8
9
"accounts": {
"emulator-account": {
"address": "YOUR ACCOUNT ADDRESS",
"privateKey": "YOUR PRIVATE KEY",
"chain": "flow-emulator",
"sigAlgorithm": "ECDSA_P256",
"hashAlgorithm": "SHA3_256"
}
},

如果你要将项目放到github或其他远端git仓库,确保别包含你的私钥。你可以使用.gitignore 避免将flow.json公开。即使我们只是使用本地仿真器,也要记得保护你的私钥。

现在发送交易,只需执行如下命令:

1
flow transactions send --code ./transactions/MintPinataParty.cdc --signer emulator-account

如果一切顺利,看起来应该是这样:

1
2
3
Getting information for account with address 0xf8d6e0586b0a20c7 ...
Submitting transaction with ID 4a79102747a450f65b6aab06a77161af196c3f7151b2400b3b3d09ade3b69823 ...
Successfully submitted transaction with ID 4a79102747a450f65b6aab06a77161af196c3f7151b2400b3b3d09ade3b69823

5、使用Cadence脚本验证NFT

现在要做的最后一件事,是验证通证并提取元数据。为此我们将编写一个简单的脚本并在命令行调用。

在项目根目录创建一个新的scripts文件夹,并在其中创建文件CheckTokenMetadata.cdc。 在文件中 添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
import PinataPartyContract from 0xf8d6e0586b0a20c7

pub fun main() : {String : String} {
let nftOwner = getAccount(0xf8d6e0586b0a20c7)
// log("NFT Owner")
let capability = nftOwner.getCapability<&{PinataPartyContract.NFTReceiver}>(/public/NFTReceiver)

let receiverRef = capability.borrow()
?? panic("Could not borrow the receiver reference")

return receiverRef.getMetadata(id: 1)
}

上面的脚本可以视为对以太坊智能合约的只读方法的调用,免费并且只是返回数据。

在我们的脚本中,从部署地址导入合约,然后定义个main函数,并在此函数内定义三个变量:

  • nftOwner: 持有NFt的账号
  • capability: 资源能力
  • receiverRef: 接收方

现在调用脚本:

1
flow scripts execute ./scripts/CheckTokenMetadata.cdc

应该可以看到如下类似内容:

1
{"name": "The Big Swing", "swing_velocity": "29", "swing_angle": "45", "rating": "5", "uri": "ipfs://QmRZdc3mAMXpv6Akz9Ekp1y4vDSjazTx2dCQRkxVy1yUj6"}

恭喜!你已经在Flow区块链成功部署Cadence合约、铸造NFT并将NFT关联的元数据保存在IPFS!


原文链接:How to Create NFTs Like NBA Top Shot With Flow and IPFS

汇智网翻译整理,转载请标明出处