• 译文出自:登链翻译计划 [1]

  • 译者:翻译小组 [2]

  • 校对:Tiny 熊 [3]

这是关于使用 Flow 和 IPFS 创建 NFT 教程的第三篇。

NFT 教程 - 如何用 IPFS 在 Flow 上创建一个 NFT 交易市场?

这是关于使用 Flow 和 IPFS 创建 NFT 教程的第三篇:

第一篇:如何用 Flow 和 IPFS 创建像 NBA Top Shot 一样的 NFT[4]

第二部分:如何展示 Flow 和 IPFS 上的 NFT 收藏品 [5]

在本系列的最后一篇,我们将通过启用 NFT 的转账来完成任务。正如你所期待的那样,Flow 有一些优秀文档,但我们将扩展这些文档,使其适合 IPFS 托管内容的模式。让我们开始吧

回顾

希望你已经跟上了前面的两篇教程。如果是这样的话,你已经有了继续学习所需的所有入门代码,我们将简单地对之前的代码进行补充。如果你还没有开始前面 2 个教程,你将会迷失方向,所以一定要回过头去去完成前面的教程。

创建合约

一个交易市场除了我们已经构建的内容之外,还需要一些内容:

  • 用同质化代币去购买 NFT

  • 代币转移能力

  • 设置代币发行量

因为 Flow 模拟器是 Flow 区块链在内存的模拟,所以要确保在这一步之前执行之前的教程,并确保模拟器保持运行。假设你已经完成了这些工作,在让我们创建一个可互换的代币合约,用于支付购买 NFT 的费用。

要明确的是,为这些同质代币创建一个购买机制不是本教程的范围内。我们只是要将代币铸造并转移到将购买 NFT 的账户中。

在本系列第一部分创建的 Pinata-party 目录下,进入 cadence/contracts 文件夹,创建一个名为 PinnieToken.cdc 的新文件。这将是我们的同质代币合约。我们将从这样的空合约开始定义。

    pub contract PinnieToken {}  

主合约代码中的每一段代码都会有自己的 Github gist,在最后了提供完整的合约代码。合约中第一块添加的是与我们的 token 和 Provider 资源相关联的 token pub 变量。

    pub var totalSupply: UFix64      pub var tokenName: String      pub resource interface Provider {          pub fun withdraw(amount: UFix64): @Vault {              post {                  result.balance == UFix64(amount):                      "Withdrawal amount must be the same as the balance of the withdrawn Vault"              }          }      }  

在我们最初创建的空合约中添加上述代码。totalSupply 和 tokenName 变量不言自明。将在稍后初始化代币合约时设置这些变量。

创建的名为 Provider 的资源接口需要多一点解释。这个资源只是简单地定义了一个公共函数,但有趣的是,它仍然只能由代币所有者调用。也就是说,我不能对你的账户执行取款请求。

接下来,我们还要定义两个公共资源接口。

    pub resource interface Receiver {          pub fun deposit(from: @Vault)      }      pub resource interface Balance {          pub var balance: UFix64      }  

把他们放在 Provider 资源接口后面, Receiver 接口包括一个任何人都可以执行的函数。只要接收者初始化了一个能够处理通过这个合约创建的代币的 Vault ,就可以执行向账户的存款。你很快就会看到对 Vault 的引用。

Balance 资源将简单地返回任何给定账户的新代币的余额。

现在创建上面提到的 Vault 资源。在 Balance 资源下面添加以下内容 :

    pub resource Vault: Provider, Receiver, Balance {          pub var balance: UFix64          init(balance: UFix64) {              self.balance = balance          }          pub fun withdraw(amount: UFix64): @Vault {              self.balance = self.balance - amount              return <-create Vault(balance: amount)          }          pub fun deposit(from: @Vault) {              self.balance = self.balance + from.balance              destroy from          }      }  

Vault 资源是关键,因为没有它,什么都不会发生。如果 Vault 资源的引用没有存储在一个账户的存储中,该账户就不能接收这些代币。这意味着该账户不能发送代币,也表示该账户就不能购买 NFT 了。

来看看 Vault 资源实现了什么。你可以看到, Vault 资源继承了 Provider Receiver Balance 资源接口,然后它定义了两个函数: withdraw deposit withdraw deposit 。如果你还记得, Provider 接口给了 withdraw 函数的访问权限,所以在这里简单地定义了这个函数。而 Receiver 接口给了 deposit 函数的访问权限,也在这里定义了。

你还会注意到,我们有一个 balance 变量,是用 Vault 资源初始化的。这个余额代表某个账户的余额。

现在,来看看如何确保一个账户能够访问 Vault 接口。记住,没有它,我们要创建的这个代币就不会发生任何事情。在 Vault 接口下面,添加以下函数:

    pub fun createEmptyVault(): @Vault {           return <-create Vault(balance: 0.0)      }  

这个函数,顾名思义,为一个账户创建一个空的 Vault 资源。当然,余额为 0。

接着加上铸币的能力,在 createEmptyVault 函数下面加上:

    pub resource VaultMinter {          pub fun mintTokens(amount: UFix64, recipient: Capability<&AnyResource;{Receiver}>) {              let recipientRef = recipient.borrow()                  ?? panic("Could not borrow a receiver reference to the vault")              PinnieToken.totalSupply = PinnieToken.totalSupply + UFix64(amount)              recipientRef.deposit(from: <-create Vault(balance: amount))          }      }  

VaultMinter 资源是公开的,但默认情况下,它只对合约账户所有者开放。可以将此资源提供给其他人,但在本教程中我们不打算重点讨论这个问题。

VaultMinter 资源只有一个功能: mintTokens 。该功能需要一个铸币量和一个接收者。只要收件人存储有 Vault 资源,新铸的代币就可以存入该账户。当代币被铸造时, totalSupply 变量必须被更新,所以我们将铸造的金额加到之前的发行量上,以获得新的发行量。

好了,我们已经做到了这一步。还有一件事要做,我们需要初始化合约。在 VaultMinter 资源后面添加这个:

    init() {          self.totalSupply = 30.0          self.tokenName = "Pinnie"          let vault <- create Vault(balance: self.totalSupply)          self.account.save(<-vault, to: /storage/MainVault)          self.account.save(<-create VaultMinter(), to: /storage/MainMinter)          self.account.link<&VaultMinter;>(/private/Minter, target: /storage/MainMinter)      }      view rawPinnieToken.cdc hosted with ❤ by GitHub  

当我们初始化合约时,需要设置一个总发行量。你可以选择的任何数字。在本例中,我们初始化的发行量为 30。我们将 tokenName 设置为 Pinnie ,因为这毕竟是关于 Pinata 派对。我们还创建了一个 vault 变量,用初始发行量创建一个 Vault 资源,并将其存储在合约创建者的账户中。

就是这样,合约完整的代码 [6]。

部署和铸造代币

我们需要更新项目中的 flow.json 文件,以便我们能够部署这个新的合约。在之前的教程中,你可能已经发现了一件事,那就是在部署合约时与执行交易时, flow.json 文件的结构需要略有不同。确保你的 flow.json 引用了新的合约,并且有 emulator-account 键引用,就像这样 :

    {       "emulators": {        "default": {         "port": 3569,         "serviceAccount": "emulator-account"        }       },       "contracts": {          "PinataPartyContract": "./cadence/contracts/PinataPartyContract.cdc",        "PinnieToken": "./cadence/contracts/PinnieToken.cdc"        },       "networks": {        "emulator": {         "host": "127.0.0.1:3569",         "chain": "flow-emulator"        }       },       "accounts": {        "emulator-account": {         "address": "f8d6e0586b0a20c7",         "keys": "e5ca2b0946358223f0555206144fe4d74e65cbd58b0933c5232ce195b9058cdd"        }       },       "deployments": {          "emulator": {            "emulator-account": ["PinataPartyContract", "PinnieToken"]          }        }      }  

在另一个终端窗口中,在 pinata-party 项目目录下,运行 flow project deploy ,会得到部署合约的账户(与 NFT 合约部署相同)。把它保存在某个地方,因为很快就会用到它。

现在,测试一下铸币功能。我们将创建一个允许铸造 Pinnie 代币的交易,但首先,需要再次更新 flow.json (可能有更好的方法)。在 emulator-account 下把你的 json 改成这样:

    "emulator-account": {           "address": "f8d6e0586b0a20c7",           "privateKey": "e5ca2b0946358223f0555206144fe4d74e65cbd58b0933c5232ce195b9058cdd",           "sigAlgorithm": "ECDSA_P256",           "hashAlgorithm": "SHA3_256",           "chain": "flow-emulator"      },  

key 字段再次变成 privateKey 字段,然后我们添加 sigAlogrithm hashAlgorithm chain 属性。不管什么原因,这种格式对发送交易有效,而另一种格式对部署合约有效。

好了,我们还需要做一件事,让部署合约的账户铸造一些 Pinnies。我们需要创建一个简单的交易,提供了对铸币功能的访问。所以,在交易文件夹里面,添加一个名为 LinkPinnie.cdc 的文件,添加以下代码:

    import PinnieToken from 0xf8d6e0586b0a20c7      transaction {        prepare(acct: AuthAccount) {          acct.link<&PinnieToken.Vault;{PinnieToken.Receiver, PinnieToken.Balance}>(/public/MainReceiver, target: /storage/MainVault)          log("Public Receiver reference created!")        }        post {          getAccount(0xf8d6e0586b0a20c7).getCapability<&PinnieToken.Vault;{PinnieToken.Receiver}>(/public/MainReceiver)                          .check():                          "Vault Receiver Reference was not created correctly"          }      }  

这个交易导入了我们的 Pinnie 合约。然后,它创建一个交易,将 Receiver 资源链接到最终将进行铸币的账号。我们将为需要引用该资源的其他账户做同样的事情。

创建好交易后,继续运行它。在项目根目录的终端中运行:

    flow transactions send --code transactions/LinkPinnie.cdc  

现在,让我们来铸造一些 Pinnies!要做到这一点,我们需要写一个交易,这个交易非常简单明了,所以我在下面放出完整的代码:

    import PinnieToken from 0xf8d6e0586b0a20c7      transaction {          let mintingRef: &PinnieToken.VaultMinter;          var receiver: Capability<&PinnieToken.Vault;{PinnieToken.Receiver}>       prepare(acct: AuthAccount) {              self.mintingRef = acct.borrow<&PinnieToken.VaultMinter;>(from: /storage/MainMinter)                  ?? panic("Could not borrow a reference to the minter")              let recipient = getAccount(0xf8d6e0586b0a20c7)              self.receiver = recipient.getCapability<&PinnieToken.Vault;{PinnieToken.Receiver}>      (/public/MainReceiver)       }          execute {              self.mintingRef.mintTokens(amount: 30.0, recipient: self.receiver)              log("30 tokens minted and deposited to account 0xf8d6e0586b0a20c7")          }      }  

这段代码应该添加到交易文件夹中, 命名为 MintPinnie.cdc 。交易在最上面导入 PinnieToken 合约,然后创建了对该合约中定义的两个资源的引用,定义了一个 VaultMinter 资源和一个 Receiver 资源等。当前就是使用的这两个资源。 VaultMinter 正如你所期望的那样,是用来铸造代币的。 Receiver 资源用于处理将新代币存入账户。

这只是一个测试,以确保可以铸造代币并将它们存入自己的账户。很快,我们将创建一个新的账户,铸造代币,并将它们存入另一个账户。

从命令行运行交易:

    flow transactions send --code /transactions/MintPinnie.cdc --signer emulator-account  

请记住,我们是用模拟器账户来部署合约的,所以除非我们提供一个 link 并允许其他账户进行铸币,否则模拟器账户是必须要进行铸币。

现在让我们创建一个脚本来检查我们的 Pinnie 余额,以确保这一切工作。在项目的脚本文件夹中,创建一个名为 CheckPinnieBalance.cdc 的文件,并添加以下内容:

    import PinnieToken from 0xf8d6e0586b0a20c7      pub fun main(): UFix64 {          let acct1 = getAccount(0xf8d6e0586b0a20c7)          let acct1ReceiverRef = acct1.getCapability<&PinnieToken.Vault;{PinnieToken.Balance}>(/public/MainReceiver)              .borrow()              ?? panic("Could not borrow a reference to the acct1 receiver")          log("Account 1 Balance")          log(acct1ReceiverRef.balance)          return acct1ReceiverRef.balance      }  

再次导入合约,这里硬编码我们想要检查的账户(模拟器账户),并且我们为 Pinnie Token 借用一个对余额资源的引用,在脚本结束时返回余额,以便在命令行中打印出来。

在创建合约的时候,设置了 30 个代币的初始发行量,所以当我们运行 MintPinnie 交易的时候,应该将额外的 30 个代币存入模拟器账户。所以当我们运行 MintPinnie 交易时,应该已经铸造并存入了额外的 30 个代币到模拟器账户中。这意味着,如果一切顺利,这个余额脚本应该显示 60 个代币。

用这个命令来运行脚本:

    flow scripts execute --code scripts/CheckPinnieBalance.cdc  

而结果应该是这样的。

    {"type":"UFix64","value":"60.00000000"}  

太棒了!我们可以铸造代币了。确保我们可以铸造一些,并将它们存入一个其他的账户

要创建一个新的账户,需要先生成一个新的密钥对。要做到这一点,请运行以下命令:

    flow keys generate  

这将生成一个私钥和一个公钥。用公钥来生成一个新的账户,很快就会使用私钥来更新 flow.json 。所以,我们现在就来创建这个新的账户。运行这个命令。

    flow accounts create --key YourNewPublicKey  

这将创建一个交易,该交易的结果将包括新的账户地址。作为创建新帐户的结果,你应该已经收到了一个交易 ID。复制该交易 ID,并运行以下命令。

    flow transactions status YourTransactionId  

这个命令的结果应该是这样的:

    Status: SEALEDEvents:Event 0: flow.AccountCreated: 0x5af6470379d5e29d7ca6825b5899def6681e66f2fe61cb49113d56406e815efaFields:address (Address): 01cf0e2f2f715450Event 1: flow.AccountKeyAdded: 0x4e9368146889999ab86aafc919a31bb9f64279176f2db247b9061a3826c5e232Fields:address (Address): 01cf0e2f2f715450publicKey (Unknown): f847b840c294432d731bfa29ae85d15442ddb546635efc4a40dced431eed6f35547d3897ba6d116d6d5be43b5614adeef8f468730ef61db5657fc8c4e5e03068e823823e8  

列出的地址是新的账户地址。让我们用它来更新 flow.json 文件。

在这个文件中,在你的 账户 对象下,为这个账户创建一个新的引用。还记得之前的私钥吗?我们现在就需要它。将你的账户对象设置成这样 :

    "accounts": {        "emulator-account": {          "address": "f8d6e0586b0a20c7",          "keys": "e5ca2b0946358223f0555206144fe4d74e65cbd58b0933c5232ce195b9058cdd"        },        "second-account": {          "address": "01cf0e2f2f715450",          "keys": "9bde7092cc0695c67f896e4375bffa0b5bf0a63ce562195a36f864ba7c3b09e3"        }      },  

我们现在有了第二个账户,可以用来发送 Pinnie 代币。让我们来看看这看起来如何。

发送代币

我们的主账户(创建 Pinnie 代币的账户)目前有 60 个代币。让我们看看是否可以将其中一些代币发送到第二个账户。

如果你还记得之前的内容,每个账户需要有一个空金库才能接受 Pinnie 代币,并且需要有一个链接到 Pinnie 代币合约上的资源。让我们从创建一个空金库开始。我们需要为此建立一个新的交易。所以,在你的 transactions 文件夹中创建一个名为 CreateEmptyPinnieVault.cdc 的文件。在该文件中,添加以下内容:

    import PinnieToken from 0xf8d6e0586b0a20c7      transaction {       prepare(acct: AuthAccount) {        let vaultA <- PinnieToken.createEmptyVault()        acct.save<@PinnieToken.Vault>(<-vaultA, to: /storage/MainVault)          log("Empty Vault stored")        let ReceiverRef = acct.link<&PinnieToken.Vault{PinnieToken.Receiver, PinnieToken.Balance}>(/public/MainReceiver, target: /storage/MainVault)          log("References created")       }          post {              getAccount(NEW_ACCOUNT_ADDRESS).getCapability<&PinnieToken.Vault;{PinnieToken.Receiver}>(/public/MainReceiver)                              .check():                              "Vault Receiver Reference was not created correctly"          }      }  

在这个交易中,我们导入 Pinnie Token 合约,调用公共函数 createEmptyVault ,并使用合约上的 Receiver 资源将其与新账户联系起来。

请注意,在 post 部分,检查下,确保将 NEW_ACCOUNT_ADDRESS 替换为刚刚创建的账户地址,并在其前面加上 0x

现在我们来运行交易。在项目的根目录下,运行 :

    flow transactions send --code transactions/CreateEmptyPinnieVault.cdc --signer second-account  

注意,我们将 signer 定义为 second-account , 确保交易由正确的账户执行的,而不是我们原来的 emulator-account 。一旦完成,就可以链接到 Pinnie Token 资源。运行以下命令 :

    flow transactions send --code transactions/LinkPinnie.cdc --signer second-account  

所有这些都已经设置好了,所以我们可以将代币从 emulator-account 转移到 second-account 。要做到这一点,我们需要另一个交易。现在就来编写这个交易。

在你的 transcations 文件夹中,创建一个名为 TransferPinnieToken.cdc 的文件。在该文件中,添加以下内容 :

    import PinnieToken from 0xf8d6e0586b0a20c7      transaction {        var temporaryVault: @PinnieToken.Vault        prepare(acct: AuthAccount) {          let vaultRef = acct.borrow<&PinnieToken.Vault;>(from: /storage/MainVault)              ?? panic("Could not borrow a reference to the owner's vault")          self.temporaryVault <- vaultRef.withdraw(amount: 10.0)        }        execute {          let recipient = getAccount(NEW_ACCOUNT_ADDRESS)          let receiverRef = recipient.getCapability(/public/MainReceiver)                            .borrow<&PinnieToken.Vault;{PinnieToken.Receiver}>()                            ?? panic("Could not borrow a reference to the receiver")          receiverRef.deposit(from: <-self.temporaryVault)          log("Transfer succeeded!")        }      }  

像往常一样,导入 Pinnie Token 合约。然后创建一个 Pinnie Token vault 的临时引用。我们这样做是因为在处理代币时,一切都发生在金库中。因此,我们需要从 emulator-account 金库中提取代币,将它们放入临时金库,然后将临时金库发送给接收方( second-account )。

在第 10 行,你可以看到我们提取和发送至 second-account 的金额是 10 个代币。

一定要将 NEW_ACCOUNT_ADDRESS 的值替换为 second-account 地址。前面加个 0x`。我们来执行交易 :

    flow transactions send --code transactions/TransferPinnieTokens.cdc --signer emulator-account  

signer 需要是 emulator-account ,因为现在只有 emulator-account 有代币。当你执行上述交易后,我们现在会有两个账户有代币。让我们来证明这一点。

打开你的 CheckPinnieBalance 脚本,将第 3 行的账户地址替换为 second-account 的地址。同样,确保你在地址前加上 0x 。保存,然后运行脚本 :

    flow scripts execute --code scripts/CheckPinnieBalance.cdc  

你应该看到以下结果。

    {        "type": "UFix64",        "value": "10.00000000"      }  

现在已经铸造了一个可以可流通代币作为货币,并且你已经将其中的一些代币转让给了另一个用户。现在,剩下的就是允许第二个账户从交易市场上购买我们的 NFT。

创建一个交易市场

更新本系列第二篇教程中的 React 代码,一个市场需要让 NFT 与 Pinnie 代币的价格一起显示,还需要一个允许用户购买 NFT 的按钮。

在进行前端代码工作之前,我们还需要创建一个合约。要想拥有一个市场,我们需要一个能够创建市场和管理市场的合约,现在就来处理这个问题。

在你的 cadence/contracts 文件夹中,创建一个新的文件,名为 MarketplaceContract.cdc 。这个合约比我们其他一些合约要大,所以将把它分成几个代码片段,最后再引用完整的合约。

首先在你的文件中加入以下内容:

    import PinataPartyContract from 0xf8d6e0586b0a20c7      import PinnieToken from 0xf8d6e0586b0a20c7      pub contract MarketplaceContract {        pub event ForSale(id: UInt64, price: UFix64)        pub event PriceChanged(id: UInt64, newPrice: UFix64)        pub event TokenPurchased(id: UInt64, price: UFix64)        pub event SaleWithdrawn(id: UInt64)        pub resource interface SalePublic {            pub fun purchase(tokenID: UInt64, recipient: &AnyResource;{PinataPartyContract.NFTReceiver}, buyTokens: @PinnieToken.Vault)            pub fun idPrice(tokenID: UInt64): UFix64?            pub fun getIDs(): [UInt64]        }      }  

导入 NFT 合约和代币合约,因为它们都将与市场合约一起工作。在合约定义里面,我们定义了四个事件。ForSale(表示 NFT 正在出售),PriceChanged(表示 NFT 的价格发生变化),TokenPurchased(表示购买了 NFT),SaleWithdrawn(表示从市场上移除了 NFT)。

在这些事件下面,我们有一个资源接口,叫做 SalePublic 。这个接口任何人都可以公开使用,而不仅仅是合约所有者。在这个接口里面公开了三个函数。

接下来,在 SalePublic 接口下面,添加一个 SaleCollection 资源。合约的关键部分,所以我不能轻易地把它分成小块。这段代码比我想的要长,但我们还是要写一遍:

    pub resource SaleCollection: SalePublic {          pub var forSale: @{UInt64: PinataPartyContract.NFT}          pub var prices: {UInt64: UFix64}          access(account) let ownerVault: Capability<&AnyResource;{PinnieToken.Receiver}>          init (vault: Capability<&AnyResource;{PinnieToken.Receiver}>) {              self.forSale <- {}              self.ownerVault = vault              self.prices = {}          }          pub fun withdraw(tokenID: UInt64): @PinataPartyContract.NFT {              self.prices.remove(key: tokenID)              let token <- self.forSale.remove(key: tokenID) ?? panic("missing NFT")              return <-token          }          pub fun listForSale(token: @PinataPartyContract.NFT, price: UFix64) {              let id = token.id              self.prices[id] = price              let oldToken <- self.forSale[id] <- token              destroy oldToken              emit ForSale(id: id, price: price)          }          pub fun changePrice(tokenID: UInt64, newPrice: UFix64) {              self.prices[tokenID] = newPrice              emit PriceChanged(id: tokenID, newPrice: newPrice)          }          pub fun purchase(tokenID: UInt64, recipient: &AnyResource;{PinataPartyContract.NFTReceiver}, buyTokens: @PinnieToken.Vault) {              pre {                  self.forSale[tokenID] != nil && self.prices[tokenID] != nil:                      "No token matching this ID for sale!"                  buyTokens.balance >= (self.prices[tokenID] ?? 0.0):                      "Not enough tokens to by the NFT!"              }              let price = self.prices[tokenID]!              self.prices[tokenID] = nil              let vaultRef = self.ownerVault.borrow()                  ?? panic("Could not borrow reference to owner token vault")              vaultRef.deposit(from: <-buyTokens)              let metadata = recipient.getMetadata(id: tokenID)              recipient.deposit(token: <-self.withdraw(tokenID: tokenID), metadata: metadata)              emit TokenPurchased(id: tokenID, price: price)          }          pub fun idPrice(tokenID: UInt64): UFix64? {              return self.prices[tokenID]          }          pub fun getIDs(): [UInt64] {              return self.forSale.keys          }          destroy() {              destroy self.forSale          }      }  

在这个资源中,我们首先要定义一些变量。我们定义了一个名为 forSale 的待售代币映射,在 prices 变量中定义了每个待售代币的价格映射,然后定义了一个只有合约所有者才能访问的保护变量,名为 ownerVault

像往常一样,在一个资源上定义变量时,需要初始化它们。所以在我们的 init 函数中进行,并简单地用空值和所有者的库资源进行初始化。

接下来是这个资源实现。定义控制我们市场所有行为动作,函数有:

  • withdraw

  • listForSale

  • changePrice

  • purchase

  • idPrice

  • getIDs

  • destroy

之前只公开了其中的三个函数,这意味着,withdraw、listForSale、changePrice 和 destroy 只有 NFT 的所有者才能使用,因为我们不希望任何人能够改变一个 NFT 的价格。

我们 Marketplace 合约的最后一部分是 createSaleCollection 函数。将一个收藏品作为资源添加到一个账户中。在 SaleCollection 资源之后,添加代码:

    pub fun createSaleCollection(ownerVault: Capability<&AnyResource;{PinnieToken.Receiver}>): @SaleCollection {        return <- create SaleCollection(vault: ownerVault)      }  

完整的合约代码在这里 [7],大家可参考。

有了这个合约后,让我们用模拟器账户来部署它。从项目的根目录下运行:

    flow project deploy  

这将部署 Marketplace 合约,并允许我们从前端应用程序中使用它。所以,让我们开始更新前端应用。

前端

正如我之前提到的,我们将在上一篇文章的基础来建立市场。所以在项目中,应该已经有一个 frontend 目录。切换到那个目录下,让看看 App.js 文件。

当前,我们拥有认证和获取单个 NFT 并显示其元数据的能力。我们想复制这个功能,但要获取存储在 Marketplace 合约中的所有代币。另外还需要加入购买功能。而且如果你拥有该代币,就能够出售代币,并改变该代币的价格。

需要修改 TokenData.js 文件来支持所有这些功能,将该文件中的所有内容替换为以下内容:

    import React, { useState, useEffect } from "react";      import * as fcl from "@onflow/fcl";      const TokenData = () => {        useEffect(() => {          checkMarketplace()        }, []);        const checkMarketplace = async () => {          try {            const encoded = await fcl.send([              fcl.script`             import MarketplaceContract from 0xf8d6e0586b0a20c7              pub fun main(): [UInt64] {                  let account1 = getAccount(0xf8d6e0586b0a20c7)                  let acct1saleRef = account1.getCapability<&AnyResource;{MarketplaceContract.SalePublic}>(/public/NFTSale)                      .borrow()                      ?? panic("Could not borrow acct2 nft sale reference")                  return acct1saleRef.getIDs()              }              `            ]);            const decoded = await fcl.decode(encoded);            console.log(decoded);          } catch (error) {            console.log("NO NFTs FOR SALE")          }        }        return (          <p className="token-data">          p>        );      };      export default TokenData;  

上面代码硬编码了一些值,所以在真正的应用程序中,一定要考虑如何动态地获取账户地址等信息。在 checkMarketplace 函数中,把所有的东西都包在了 try/catch 中。这是因为 fcl.send 函数会在没有 NFT 陈列销售时抛出。

如果通过前端目录并运行 npm start 来启动前端应用程序,会在控制台中看到 NO NFT FOR SALE

让我们来解决这个问题!

为了简洁起见,我们将通过 Flow CLI 工具列出铸造的 NFT。不过,你也可以扩展本教程,改为通过在用户界面上进行。在你的 pinata-party 项目根目录下,在 transactions 文件夹内,创建一个名为 ListTokenForSale.cdc 的文件,添加以下内容:

    import PinataPartyContract from 0xf8d6e0586b0a20c7      import PinnieToken from 0xf8d6e0586b0a20c7      import MarketplaceContract from 0xf8d6e0586b0a20c7      transaction {          prepare(acct: AuthAccount) {              let receiver = acct.getCapability<&{PinnieToken.Receiver}>(/public/MainReceiver)              let sale <- MarketplaceContract.createSaleCollection(ownerVault: receiver)              let collectionRef = acct.borrow<&PinataPartyContract.Collection;>(from: /storage/NFTCollection)                  ?? panic("Could not borrow owner's nft collection reference")              let token <- collectionRef.withdraw(withdrawID: 1)              sale.listForSale(token: <-token, price: 10.0)              acct.save(<-sale, to: /storage/NFTSale)              acct.link<&MarketplaceContract.SaleCollection;{MarketplaceContract.SalePublic}>(/public/NFTSale, target: /storage/NFTSale)              log("Sale Created for account 1. Selling NFT 1 for 10 tokens")          }      }  

在这个交易中,导入我们创建的所有三个合约。因为要接受 PinnieToken 的付款,因为需要 PinnieToken Receiver 能力。我们还需要获得 MarketplaceContract 上的 createSaleCollection 函数的访问权限。然后,需要引用我们要挂牌出售的 NFT。赎回该 NFT,以 10.0PinnieTokens 的价格将其挂牌出售,并将其保存到 NFTSale 存储路径中。

行下面的命令,你应该可以成功地列出之前铸造的 NFT。

    flow transactions execute --code transactions/ListTokenForSale.cdc  

现在,回到你的 React App 页面并刷新。在控制台中,你应该看到这样的东西。

NFT 教程 - 如何用 IPFS 在 Flow 上创建一个 NFT 交易市场?

它列出了指定账户地址的所有 tokenID 的数组,这些 tokenID 被列出出售,以便知道要查找和获取元数据的 ID。在这里,只有一个 token 被列出,由于我们只创建了这一个 token,所以它的 tokenID 是 1。

在我们为 React App 添加代码之前,在 TokenData.js 文件的顶部添加以下导入。

    import * as t from "@onflow/types"  

这使我们能够向使用 fcl 发送的脚本传递参数。

好了,现在我们可以使用的 tokenID 数组,并利用之前的一些代码来获取 token 元数据。在 TokenData.js 文件和 checkMarketplace 函数中,在 decoded 变量后添加以下内容:

    for (const id of decoded) {          const encodedMetadata = await fcl.send([            fcl.script`              import PinataPartyContract from 0xf8d6e0586b0a20c7              pub fun main(id: Int) : {String : String} {                let nftOwner = getAccount(0xf8d6e0586b0a20c7)                let capability = nftOwner.getCapability<&{PinataPartyContract.NFTReceiver}>(/public/NFTReceiver)                let receiverRef = capability.borrow()                    ?? panic("Could not borrow the receiver reference")                return receiverRef.getMetadata(id: 1)              }            `,            fcl.args([              fcl.arg(id, t.Int)            ]),          ]);          const decodedMetadata = await fcl.decode(encodedMetadata);          marketplaceMetadata.push(decodedMetadata);        }        console.log(marketplaceMetadata);  

如果在控制台中查看,现在应该看到一个专门与出售的代币相关联的元数据数组。在我们渲染任何内容之前,我们需要做的最后一件事是列出的代币的价格。

decodedMetadata 变量下面和 marketplaceMetadata.push(decodedMetadata) 函数之前,添加以下内容:

    const encodedPrice = await fcl.send([        fcl.script`          import MarketplaceContract from 0xf8d6e0586b0a20c7          pub fun main(id: UInt64): UFix64? {              let account1 = getAccount(0xf8d6e0586b0a20c7)              let acct1saleRef = account1.getCapability<&AnyResource;{MarketplaceContract.SalePublic}>(/public/NFTSale)                  .borrow()                  ?? panic("Could not borrow acct nft sale reference")              return acct1saleRef.idPrice(tokenID: id)          }        `,        fcl.args([          fcl.arg(id, t.UInt64)        ])      ])      const decodedPrice = await fcl.decode(encodedPrice)      decodedMetadata["price"] = decodedPrice;      marketplaceMetadata.push(decodedMetadata);  

我们正在获取每个已经上市的 NFT 的价格,当收到价格时,我们将其添加到代币元数据中,然后再将该元数据推送到 marketplaceMetadata 数组中。

现在在控制台,应该看到这样的东西:

NFT 教程 - 如何用 IPFS 在 Flow 上创建一个 NFT 交易市场?

很棒,我们现在可以渲染代币并显示价格了,在显示 marketplaceMetadata 数组的 console.log 语句下,添加以下内容:

    setTokensToSell(marketplaceMetadata)  

还需要在 TokenData 主函数声明的开头添加以下内容:

    const TokenData = () => {  const [tokensToSell, setTokensToSell] = useState([])      }  

有了这些东西,你就可以渲染你的市场了。在 return 语句中,添加以下内容。

    return (        <p className="token-data">          {            tokensToSell.map(token => {              return (                <p key={token.uri} className="listing">                  <p>                    <h3>{token.name}h3>                    <h4>Statsh4>                    <p>Overall Rating: {token.rating}p>                    <p>Swing Angle: {token.swing_angle}p>                    <p>Swing Velocity: {token.swing_velocity}p>                    <h4>Videoh4>                    <video loop="true" autoplay="" playsinline="" preload="auto" width="85%">                      <source hide={`https://ipfs.io/ipfs/${token["uri"].split("://")[1]}`} type="video/mp4" />                    video>                    <h4>Priceh4>                    <p>{parseInt(token.price, 10).toFixed(2)} Pinniesp>                    <button className="btn-primary">Buy Nowbutton>                  p>                p>              )            })          }        p>      );  

以下是我使用的样式,添加到 App.css 文件中:

    .listing {        max-width: 30%;        padding: 50px;        margin: 2.5%;        box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);      }  

应用程序现在看起来应该是这样的:

NFT 教程 - 如何用 IPFS 在 Flow 上创建一个 NFT 交易市场?

最后我们需要做的是连接那个 Buy Now 按钮,让不是 NFT 所有者的人购买 NFT。

购买 NFT

通常情况下,需要通过一个远程发现节点端点来进行钱包发现和交易处理,实际上,在第二篇设置了它。我们现在使用的是本地 Flow 模拟器。因此,我们需要运行一个本地开发者钱包,然后更新相应的环境变量。

首先,克隆本地开发者钱包。在 pinata-party 项目的根目录下运行:

    git clone git@github.com:onflow/fcl-dev-wallet.git  

进入文件夹:

    cd fcl-dev-wallet  

现在,我们需要复制样本 env 文件,并创建我们的本地 env 文件,开发钱包将使用。

    cp .env.example .env.local  

安装依赖关系:

    npm install  

好的,完成后,打开 .env.local 文件,你会看到它引用了一个账户和一个私钥。之前,我们创建了一个新的账户,将从市场上购买 NFT。修改 .env.local 文件中的账户,使其与你创建的新账户相匹配。对于 FLOW_ACCOUNT_KEY_ID 环境变量,将其改为 1,模拟器账户的密钥为 0。

现在,你可以运行 npm run dev 来启动钱包服务器。

回到项目的 frontend 目录下,找到 .env 文件,让我们更新 REACT_APP_WALLET_DISCOVERY 指向 http://localhost:3000/fcl/authz 。做完这些,你需要重新启动 React 应用。

下一步是连接前端的 ”Buy Now“ 按钮,以实际发送交易来购买 token。打开 TokenData.js 文件,创建一个像这样的 buyToken 函数:

    const buyToken = async (tokenId) => {        const txId = await fcl        .send([          fcl.proposer(fcl.authz),          fcl.payer(fcl.authz),          fcl.authorizations([fcl.authz]),          fcl.limit(50),          fcl.args([            fcl.arg(tokenId, t.UInt64)          ]),          fcl.transaction`            import PinataPartyContract from 0xf8d6e0586b0a20c7            import PinnieToken from 0xf8d6e0586b0a20c7            import MarketplaceContract from 0xf8d6e0586b0a20c7            transaction {                let collectionRef: &AnyResource;{PinataPartyContract.NFTReceiver}                let temporaryVault: @PinnieToken.Vault                prepare(acct: AuthAccount) {                    self.collectionRef = acct.borrow<&AnyResource;{PinataPartyContract.NFTReceiver}>(from: /storage/NFTCollection)!                    let vaultRef = acct.borrow<&PinnieToken.Vault;>(from: /storage/MainVault)                        ?? panic("Could not borrow owner's vault reference")                    self.temporaryVault <- vaultRef.withdraw(amount: 10.0)                }                execute {                    let seller = getAccount(0xf8d6e0586b0a20c7)                    let saleRef = seller.getCapability<&AnyResource;{MarketplaceContract.SalePublic}>(/public/NFTSale)                        .borrow()                        ?? panic("Could not borrow seller's sale reference")                    saleRef.purchase(tokenID: tokenId, recipient: self.collectionRef, buyTokens: <-self.temporaryVault)                }            }          `,        ])        await fcl.decode(txId);        checkMarketplace();      }  

现在,我们只需要为 Buy Now 按钮添加一个 onClick 处理程序。这很简单,只要将按钮更新成:

     buyToken(1)} className="btn-primary">Buy Now  

我们在这里对 tokenID 进行了硬编码,但你可以很容易地从我们早期执行的脚本中获取。

现在,当你进入你的 React 应用并点击 Buy Now 按钮时,你应该看到这样的屏幕。

NFT 教程 - 如何用 IPFS 在 Flow 上创建一个 NFT 交易市场?

正如开头中所说的那样,fcl-dev-wallet 人处于 alpha 状态,所以事实是,交易的执行可能最终成功,也可能不成功。但走到这一步,说明你的应用确实能用,fcl 库确实能用。

结论

本篇特别长,但我希望它们能帮助说明如何结合 IPFS 和 Flow 的力量来创建由可验证的标识符支持的 NFT。

如果你在使用本教程或其他任何教程时遇到问题,我强烈建议你用 Flow Playground[8] 进行实验。它真的很神奇。你可能还想绕过模拟器测试,在 Playground 工作后开始在 Testnet 上测试。

无论你做什么,我都希望你能带着更多的知识离开,了解我们如何推动 NFT 空间的发展。如果你想访问所有这些教程的完整源代码,在这里 [9] 获取。


本翻译由 Cell Network[10] 赞助支持。

来源: https://medium.com/pinata/how-to-create-an-nft-marketplace-on-flow-with-ipfs-a162a1aeb426

参考资料

[1]

登链翻译计划 : https://github.com/lbc-team/Pioneer

[2]

翻译小组 : https://learnblockchain.cn/people/412

[3]

Tiny 熊 : https://learnblockchain.cn/people/15

[4]

如何用 Flow 和 IPFS 创建像 NBA Top Shot 一样的 NFT: https://learnblockchain.cn/article/2271

[5]

如何展示 Flow 和 IPFS 上的 NFT 收藏品 : https://learnblockchain.cn/article/2276

[6]

合约完整的代码 : https://gist.github.com/polluterofminds/d9e98584e260cdbaf474504f3ee39284

[7]

合约代码在这里 : https://gist.github.com/polluterofminds/66969996ce62ae152d2a3f08ce6694d4

[8]

Flow Playground: https://play.onflow.org/

[9]

在这里 : https://github.com/PinataCloud/Flow_NFT_IPFS

[10]

Cell Network: https://www.cellnetwork.io/?utm_souce=learnblockchain

NFT 教程 - 如何用 IPFS 在 Flow 上创建一个 NFT 交易市场?