本文转载自微信公众号「区块链研究实验室」,作者链三丰。转载本文请联系区块链研究实验室公众号。
随着不可替代令牌(NFT)市场达到高潮,回顾一下相对较早的NFT并记住CryptoKitties面临的挑战是很有趣的。由Dapper Labs团队构建的平台是潜在大规模采用的第一个真实示例,也是以太坊区块链上的第一个真实压力。
从那时起,NFT开始兴起,诸如Rarible,OpenSea,Foundation和Sorare之类的平台也开始兴起。这些平台每月有数百万美元的资金流向。尽管有早期的挣扎,但大多数情况还是在以太坊区块链上发生的。但是,Dapper Labs的团队在使用CryptoKitties的经验之后,着手建立一个新的区块链,该区块链将是通用的,但也非常适合NFT用例。这样做的目的是解决以太坊上的NFT所遇到的许多问题,同时为该领域的开发商和收藏家提供更好的体验。
今天我们讨论如何在由IPFS支持的Flow上创建NFT。Flow区块链的主要早期应用之一是NBA Top Shot。我们将构建NFT铸造过程的非常基本的副本,然后在IPFS上备份NFT元数据和资产。
这是一个包含以下三部分的教程:
- 创建合约并铸造代币
- 创建一个应用以查看通过此合同创建的NFT
- 创建一个市场以将NFT转让给其他人,同时也转让NFT在IPFS上的基础资产
今天让我们开始第一个教程。
配置
我们需要安装Flow CLI:
苹果系统:
- brew install flow-cli
Linux:
- sh -ci “$(curl -fsSL https://storage.googleapis.com/flow-cli/install.sh)"
Windows:
- iex “& { $(irm ‘https://storage.googleapis.com/flow-cli/install.ps1') }”
我们将在IPFS上存储资产文件。为了简化操作,我们可以使用Pinata。您可以在此处注册一个免费帐户,并在此处获取API密钥。在本教程的第二篇文章中,我们将使用API,但在这篇文章中,我们将使用Pinata网站。
我们还需要安装NodeJS和文本编辑器,以帮助突出显示Flow智能合约代码(以Cadence语言编写)的语法。您可以在此处安装Node。Visual Studio Code具有支持Cadence的扩展。
安装好之后,让我们创建一个目录来容纳我们的项目:
- mkdir pinata-party
转到该目录并初始化一个新的流程项目:
- cd pinata-party
- flow project init
现在,在您喜欢的代码编辑器中打开项目(同样,如果您使用Visual Studio Code,请抓住Cadence扩展),然后开始工作。
您会看到一个flow.json文件,我们将很快使用它。首先,创建一个名为的文件夹cadence。在该文件夹中,添加另一个名为的文件夹contracts。最后,在contracts名为的文件夹中创建一个文件PinataPartyContract.cdc。
在继续前进之前,重要的一点是要指出,从现在开始,我们对Flow区块链所做的一切都将在模拟器上完成。但是,将项目部署到testnet或mainnet就像更新flow.json文件中的配置设置一样简单。现在让我们为仿真器环境设置该文件,然后就可以开始编写合同了。
更新合同对象,flow.json如下所示:
- "contracts": {
- "PinataPartyContract": "./cadence/contracts/PinataPartyContract.cdc"
- }
然后,更新该deployments文件中的对象,如下所示:
- "deployments": {
- "emulator": {
- "emulator-account": ["PinataPartyContract"]
- }
- }
这是在告诉Flow CLI使用仿真器来部署我们的合同,它还引用了该帐户(在仿真器上)和我们即将写的合同。
合约
Flow提供了有关创建NFT合约的出色教程。这是一个很好的参考点,但是正如Flow指出的那样,他们尚未解决NFT元数据问题。他们想在链上存储元数据。那是个好主意,他们一定会提出一个合乎逻辑的方法。但是,我们现在想用元数据创建一些令牌,并且我们希望与NFT相关联的媒体文件。元数据只是一个组成部分。我们还需要指出令牌最终代表的媒体。
如果您熟悉以太坊区块链上的NFT,您可能会知道这些令牌返回的许多资产都存储在传统数据存储和云托管提供商中。可以,除非不是。过去我们曾写过关于内容可寻址内容的天才之处,以及在传统云平台上存储与区块链相邻的数据的弊端。归结为两点:
- 资产应该是可验证的
- 维护职责的转移应该很容易
IPFS照顾了这两个方面。然后,Pinata以一种简单的方式分层,以将该内容长期固定在IPFS上。这正是我们想要支持NFT的媒体所需要的,对吗?我们要确保可以证明所有权(NFT),提供有关NFT(NFT)的数据,并确保我们对基础资产(IPFS)(介质或其他)拥有控制权,而不是某些副本的控制权。
考虑到所有这些,让我们写一个合约,创建NFT,将元数据与NFT相关联,并确保元数据指向存储在IPFS上的基础资产。
打开PinataPartyContract.cdc,让我们开始工作。
- pub contract PinataPartyContract {
- pub resource NFT {
- pub let id: UInt64
- init(initID: UInt64) {
- self.id = initID
- }
- }
- }
第一步是定义我们的合同。我们将为此添加更多的内容,但是我们首先定义PinataPartyContract并在其中创建一个resource。资源是存储在用户帐户中的项目,可以通过访问控制措施进行访问。在这种情况下,NFT资源最终是因为拥有用于表示NFT的事物而拥有的。NFT必须是唯一可识别的。该id属性使我们能够识别令牌。
接下来,我们需要创建一个资源接口,该接口将用于定义哪些功能可供其他人(即不是合同所有者的人)使用:
- 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的钱包。
- 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
- }
- }
此资源中有很多事情要做,但是应该很快就有意义。首先,我们有一个名为的变量ownedNFTs。这很简单。它跟踪该合同中用户拥有的所有NFT。
接下来,我们有一个名为的变量metadataObjs。这一点有点独特,因为我们正在扩展Flow NFT合同功能,以存储每个NFT的元数据映射。此变量将令牌ID映射到其关联的元数据,这意味着我们需要先设置令牌ID,然后才能进行设置。
然后,我们初始化变量。对于Flow中资源中定义的变量,这是必需的。
最后,我们拥有NFT收集资源的所有可用功能。请注意,并非所有这些功能都可以使用。如果您还记得的话,我们在NFTReceiver资源界面的前面定义了任何人都可以使用的功能。
我确实要指出deposit功能。正如我们将默认的Flow NFT合同扩展为包括metadataObjs映射一样,我们也在扩展默认deposit函数以采用的附加参数metadata。我们为什么在这里这样做?我们需要确保只有令牌的铸造者才能将该元数据添加到令牌中。为了保持私密性,我们将最初添加的元数据限制在铸造执行中。
我们的合同代码几乎完成了。因此,在Collection资源下方,添加以下内容:
- 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集合。这样,首次与我们的合同进行交互的用户将具有一个创建到Collection我们定义的资源的存储位置。
之后,我们再创建一个资源。这很重要,因为没有它,我们将无法铸造代币。该NFTMinter资源包括idCount其递增,以确保我们从来没有对我们的NFT重复的ID。它还具有实际创建我们的NFT的功能。
在NFTMinter资源下方,添加主合同初始化程序:
- 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)
- }
仅在部署合同时才调用此初始化函数。它做三件事:
- 为集合的部署者创建一个空的集合,以便合同的所有者可以创建该合同的NFT并拥有该NFT。
- Collection参考NFTReceiver我们在开始时创建的界面,该资源在公共位置发布。这就是我们告诉合同的方式,NFTReceiver任何人都可以调用上定义的功能。
- 该NFTMinter资源被保存在账户储存合同的创造者。这意味着只有合同的创建者才能铸造代币。
完整的合同可以在这里找到。
现在我们准备好了合同,让我们部署它,对吧?好吧,我们可能应该在Flow Playground上对其进行测试。转到那里,然后单击左侧边栏中的第一个帐户。用我们的合同代码替换示例合同中的所有代码,然后单击“部署”。如果一切顺利,您应该在屏幕底部的日志窗口中看到这样的日志:
- flow project start-emulator
现在,我们准备将合同部署到本地运行的模拟器。在命令行中,运行以下命令:
- flow project deploy
现在,在仿真器运行且flow.json文件配置正确的情况下,我们可以部署合同。只需运行以下命令:
flow project deploy
如果一切顺利,您应该会看到类似以下的输出:
Deploying 1 contracts for accounts: emulator-account
PinataPartyContract -> 0xf8d6e0586b0a20c7
现在,我们在Flow仿真器上有一个合同,但是我们想要铸造一个令牌。
铸造NFT
在本教程的第二篇文章中,我们将致力于通过应用程序和用户界面使铸造过程更加用户友好。为了说明问题,并显示元数据如何与Flow上的NFT一起使用,我们将使用Cadence脚本和命令行。
让我们在pinata-party项目的根目录下创建一个新目录,并将其称为transactions。创建该文件夹后,在其中创建一个名为的新文件MintPinataParty.cdc。
为了编写交易,我们需要在提供给NFT的元数据中引用一个文件。为此,我们将通过Pinata将文件上传到IPFS,您可以上传任何想要的视频文件。您可以真正上载任何资产文件并将其与NFT关联。
上传文件后,系统会为您提供IPFS哈希(通常称为内容标识符或CID)。复制此哈希,因为我们将在铸造过程中使用它。
现在,在MintPinataParty.cdc文件内添加以下内容:
- 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")
- }
- }
这是一个非常简单的事务,这在很大程度上要感谢Flow为使事情变得容易而进行的工作,但让我们逐步进行一下。首先,您会在顶部注意到import语句。如果您还记得的话,当我们部署合同时,我们会收到一个帐户。这就是我们需要参考的内容。因此,请替换0xf8d6e0586b0a20c7为您部署中的帐户地址。
接下来,我们定义交易。这里发生的一切都与我们计划执行的交易有关。
我们在交易中要做的第一件事是定义两个参考变量receiverRef和minterRef。在这种情况下,我们既是NFT的接收者又是NFT的铸造者。这两个变量引用了我们在合同中创建的资源。如果执行事务的人无权访问该资源,则事务将失败。
接下来,我们有一个prepare功能。此功能获取尝试执行交易的人员的帐户信息并进行一些验证。我们尝试“借用”我们定义的NFTMinter和两种资源上的可用功能NFTReceiver。如果执行交易的人没有访问这些资源的权限,那么事情将会失败。
最后,我们有我们的execute功能。此功能是我们为NFT建立元数据,创建NFT,然后关联元数据,然后再将NFT存入我们的帐户的地方。如果您注意到的话,我创建了一个元数据变量。在该变量中,我添加了一些有关令牌的信息。由于我们的代币表示在聚会上砸了一个披萨的事件,并且由于我们试图复制您在NBA Top Shot中看到的大部分内容,因此我在元数据中定义了一些统计信息。孩子挥动棍子打皮纳塔的速度,挥杆角度和等级。我只是在玩这些统计数据。但是,您将以类似的方式输入对您的令牌有意义的任何信息。
您会注意到,我也在uri元数据中定义了一个属性。这将指向承载与NFT关联的资产文件的IPFS哈希。在这种情况下,这是被击中的Pi?ata的实际视频。您可以使用之前上传文件后收到的哈希值替换哈希值。
ipfs://出于几个原因,我们为哈希添加前缀。这是IPFS上文件的正确参考,可以与IPFS的桌面客户端和浏览器扩展一起使用。现在,我们为IPFS内容提供了本机支持,我们也可以将其直接粘贴到Brave浏览器中。
我们调用mintNFT创建令牌的函数。然后,我们必须调用该deposit函数以将其放入我们的帐户。这也是我们传递元数据的地方。请记住,我们在deposit函数中定义了一个变量关联,该关联将元数据添加到关联的令牌ID。
最后,我们简单地注销令牌已铸造和存放的事实。
现在,我们几乎准备发送交易并创建NFT。但是首先,我们需要准备我们的账户。从项目的根文件夹中的命令行,让我们创建一个用于签名的新私钥。
运行以下命令:
- flow keys generate
这将为您提供一个公钥和一个私钥。**始终保护您的私钥**
我们将需要私钥来签署交易,因此我们可以将其粘贴到我们的flow.json文件中。我们还需要指定签名算法。这是文件中的accounts对象flow.json现在应如下所示:
- "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。即使我们仅使用本地仿真器,还是保护您的密钥的一种很好的做法。
现在我们已经更新了,我们可以发送交易了。这样做就像运行以下命令一样简单:
- flow transactions send --code ./transactions/MintPinataParty.cdc --signer emulator-account
我们从中引用了我们编写的交易文件和签名人帐户flow.json。如果一切顺利,您应该会看到类似以下的输出:
Getting information for account with address 0xf8d6e0586b0a20c7 ...
Submitting transaction with ID 4a79102747a450f65b6aab06a77161af196c3f7151b2400b3b3d09ade3b69823 ...
Successfully submitted transaction with ID 4a79102747a450f65b6aab06a77161af196c3f7151b2400b3b3d09ade3b69823
现在,我们要做的最后一件事是验证令牌是否在我们的帐户中并获取元数据。这样做,我们将编写一个非常简单的脚本并从命令行调用它。
在项目的根目录中,创建一个名为的新文件夹scripts。在其中创建一个名为的文件CheckTokenMetadata.cdc。在该文件中,添加以下内容:
- 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的帐户。我们从也部署了合同的帐户中铸造了NFT,因此在我们的示例中,这两个地址是相同的。取决于将来的合同设计,这可能并不总是正确的。
- 功能:我们需要从已部署的合同中“借用”可用的功能(或功能)。请记住,这些功能是受访问控制的,因此,如果某功能对于尝试借用它的地址不可用,则脚本将失败。我们正在从NFTReceiver资源中借用能力。
- receiverRef:该变量只是利用我们的能力,并告诉脚本从已部署的合同中借用。
现在,我们可以调用函数(可用的函数)了。在这种情况下,我们要确保所讨论的地址实际上已收到我们铸造的NFT,然后我们要查看与令牌关联的元数据。
让我们运行我们的脚本,看看我们得到了什么。在命令行上运行以下命令:
- flow scripts execute ./scripts/CheckTokenMetadata.cdc
对于元数据输出,您应该看到类似以下的输出:
- {"name": "The Big Swing", "swing_velocity": "29", "swing_angle": "45", "rating": "5", "uri": "ipfs://QmRZdc3mAMXpv6Akz9Ekp1y4vDSjazTx2dCQRkxVy1yUj6"}
恭喜你!您已成功创建Flow智能合约,铸造了令牌以及与该令牌相关的元数据,并将该令牌的基础数字资产存储在IPFS上。接下来第二个部分的教程将构建一个简单的React应用程序,该应用程序与Flow智能合约对接,以进行身份验证并获取用户拥有的NFT,敬请期待。