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

第 3 部分, 渲染并交易 NFT

> * 原文:https://medium.com/pinata/how-to-create-an-nft-marketplace-on-flow-with-ipfs-a162a1aeb426 > * 译文出自:[登链翻译计划](https://github.com/lbc-team/Pioneer) > * 译者:[翻译小组](https://learnblockchain.cn/people/412) > * 校对:[Tiny 熊](https://learnblockchain.cn/people/15) > * 本文永久链接:[learnblockchain.cn/article…](https://learnblockchain.cn/article/2319) > 这是关于使用Flow和IPFS创建NFT教程的第三篇。 ![image-20210316085604963](https://img.learnblockchain.cn/pics/20210316085612.png) 这是关于使用Flow和IPFS创建NFT教程的第三篇: 第一篇:[如何用Flow和IPFS创建像NBA Top Shot一样的NFT](https://learnblockchain.cn/article/2271) 第二部分:[如何展示Flow和 IPFS 上的NFT收藏品](https://learnblockchain.cn/article/2276) 在本系列的最后一篇,我们将通过启用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 `函数下面加上: ```javascript 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`资源,并将其存储在合约创建者的账户中。 就是这样,[合约完整的代码](https://gist.github.com/polluterofminds/d9e98584e260cdbaf474504f3ee39284)。 ## 部署和铸造代币 我们需要更新项目中的`flow.json`文件,以便我们能够部署这个新的合约。在之前的教程中,你可能已经发现了一件事,那就是在部署合约时与执行交易时,`flow.json`文件的结构需要略有不同。确保你的 `flow.json`引用了新的合约,并且有`emulator-account`键引用,就像这样: ```javascript { "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`的文件,添加以下代码: ```javascript 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!要做到这一点,我们需要写一个交易,这个交易非常简单明了,所以我在下面放出完整的代码: ```javascript 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 `的文件,并添加以下内容: ```javascript 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,并运行以下命令。 ```bash flow transactions status YourTransactionId ``` 这个命令的结果应该是这样的: ```bash Status: SEALEDEvents:Event 0: flow.AccountCreated: 0x5af6470379d5e29d7ca6825b5899def6681e66f2fe61cb49113d56406e815efaFields:address (Address): 01cf0e2f2f715450Event 1: flow.AccountKeyAdded: 0x4e9368146889999ab86aafc919a31bb9f64279176f2db247b9061a3826c5e232Fields:address (Address): 01cf0e2f2f715450publicKey (Unknown): f847b840c294432d731bfa29ae85d15442ddb546635efc4a40dced431eed6f35547d3897ba6d116d6d5be43b5614adeef8f468730ef61db5657fc8c4e5e03068e823823e8 ``` 列出的地址是新的账户地址。让我们用它来更新`flow.json`文件。 在这个文件中,在你的 `账户 `对象下,为这个账户创建一个新的引用。还记得之前的私钥吗?我们现在就需要它。将你的账户对象设置成这样: ```json "accounts": { "emulator-account": { "address": "f8d6e0586b0a20c7", "keys": "e5ca2b0946358223f0555206144fe4d74e65cbd58b0933c5232ce195b9058cdd" }, "second-account": { "address": "01cf0e2f2f715450", "keys": "9bde7092cc0695c67f896e4375bffa0b5bf0a63ce562195a36f864ba7c3b09e3" } }, ``` 我们现在有了第二个账户,可以用来发送Pinnie代币。让我们来看看这看起来如何。 ## 发送代币 我们的主账户(创建Pinnie代币的账户)目前有60个代币。让我们看看是否可以将其中一些代币发送到第二个账户。 如果你还记得之前的内容,每个账户需要有一个空金库才能接受Pinnie代币,并且需要有一个链接到Pinnie代币合约上的资源。让我们从创建一个空金库开始。我们需要为此建立一个新的交易。所以,在你的`transactions`文件夹中创建一个名为`CreateEmptyPinnieVault.cdc`的文件。在该文件中,添加以下内容: ```javascript 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) } ``` 完整的[合约代码在这里](https://gist.github.com/polluterofminds/66969996ce62ae152d2a3f08ce6694d4),大家可参考。 有了这个合约后,让我们用模拟器账户来部署它。从项目的根目录下运行: ``` flow project deploy ``` 这将部署Marketplace合约,并允许我们从前端应用程序中使用它。所以,让我们开始更新前端应用。 # 前端 正如我之前提到的,我们将在上一篇文章的基础来建立市场。所以在项目中,应该已经有一个`frontend`目录。切换到那个目录下,让看看`App.js`文件。 当前,我们拥有认证和获取单个NFT并显示其元数据的能力。我们想复制这个功能,但要获取存储在Marketplace合约中的所有代币。另外还需要加入购买功能。而且如果你拥有该代币,就能够出售代币,并改变该代币的价格。 需要修改`TokenData.js`文件来支持所有这些功能,将该文件中的所有内容替换为以下内容: ```javascript 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 ( <div className="token-data"> </div> ); }; 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页面并刷新。在控制台中,你应该看到这样的东西。 ![](https://img.learnblockchain.cn/2021/03/30/16170881403805.jpg) 它列出了指定账户地址的所有tokenID的数组,这些tokenID被列出出售,以便知道要查找和获取元数据的ID。在这里,只有一个token被列出,由于我们只创建了这一个token,所以它的tokenID是1。 在我们为React App添加代码之前,在`TokenData.js`文件的顶部添加以下导入。 ``` import * as t from "@onflow/types" ``` 这使我们能够向使用`fcl`发送的脚本传递参数。 好了,现在我们可以使用的tokenID数组,并利用之前的一些代码来获取token元数据。在`TokenData.js`文件和`checkMarketplace`函数中,在`decoded`变量后添加以下内容: ```javascript 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) `函数之前,添加以下内容: ```javascript 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`数组中。 现在在控制台,应该看到这样的东西: ![1_FRi6Pwe_KmESj2T_CgtjLA](https://img.learnblockchain.cn/pics/20210330143828.png) 很棒,我们现在可以渲染代币并显示价格了,在显示`marketplaceMetadata`数组的console.log语句下,添加以下内容: ``` setTokensToSell(marketplaceMetadata) ``` 还需要在`TokenData`主函数声明的开头添加以下内容: ``` const TokenData = () => { const [tokensToSell, setTokensToSell] = useState([]) } ``` 有了这些东西,你就可以渲染你的市场了。在`return`语句中,添加以下内容。 ```javascript return ( <div className="token-data"> { tokensToSell.map(token => { return ( <div key={token.uri} className="listing"> <div> <h3>{token.name}</h3> <h4>Stats</h4> <p>Overall Rating: {token.rating}</p> <p>Swing Angle: {token.swing_angle}</p> <p>Swing Velocity: {token.swing_velocity}</p> <h4>Video</h4> <video loop="true" autoplay="" playsinline="" preload="auto" width="85%"> <source src={`https://ipfs.io/ipfs/${token["uri"].split("://")[1]}`} type="video/mp4" /> </video> <h4>Price</h4> <p>{parseInt(token.price, 10).toFixed(2)} Pinnies</p> <button className="btn-primary">Buy Now</button> </div> </div> ) }) } </div> ); ``` 以下是我使用的样式,添加到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); } ``` 应用程序现在看起来应该是这样的: ![](https://img.learnblockchain.cn/pics/20210330144427.png) 最后我们需要做的是连接那个 `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函数: ```javascript 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 `处理程序。这很简单,只要将按钮更新成: ``` <button onClick={() => buyToken(1)} className="btn-primary">Buy Now</button> ``` 我们在这里对tokenID进行了硬编码,但你可以很容易地从我们早期执行的脚本中获取。 现在,当你进入你的React应用并点击 `Buy Now `按钮时,你应该看到这样的屏幕。 ![1_cL74JHAa5-ao6HoLnagOGg](https://img.learnblockchain.cn/pics/20210330145703.png) 正如开头中所说的那样,fcl-dev-wallet 人处于alpha状态,所以事实是,交易的执行可能最终成功,也可能不成功。但走到这一步,说明你的应用确实能用,fcl库确实能用。 ## 结论 本篇特别长,但我希望它们能帮助说明如何结合IPFS和Flow的力量来创建由可验证的标识符支持的NFT。 如果你在使用本教程或其他任何教程时遇到问题,我强烈建议你用[Flow Playground](https://play.onflow.org/)进行实验。它真的很神奇。你可能还想绕过模拟器测试,在Playground工作后开始在Testnet上测试。 无论你做什么,我都希望你能带着更多的知识离开,了解我们如何推动NFT空间的发展。如果你想访问所有这些教程的完整源代码,[在这里](https://github.com/PinataCloud/Flow_NFT_IPFS) 获取。 --- 本翻译由 [Cell Network](https://www.cellnetwork.io/?utm_souce=learnblockchain) 赞助支持。

  • 原文:https://medium.com/pinata/how-to-create-an-nft-marketplace-on-flow-with-ipfs-a162a1aeb426
  • 译文出自:登链翻译计划
  • 译者:翻译小组
  • 校对:Tiny 熊
  • 本文永久链接:learnblockchain.cn/article…

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

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

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

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

在本系列的最后一篇,我们将通过启用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 &lt;-create Vault(balance: amount)
    }

    pub fun deposit(from: @Vault) {
        self.balance = self.balance + from.balance
        destroy from
    }
}

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

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

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

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

pub fun createEmptyVault(): @Vault {
     return &lt;-create Vault(balance: 0.0)
}

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

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

pub resource VaultMinter {
    pub fun mintTokens(amount: UFix64, recipient: Capability&lt;&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: &lt;-create Vault(balance: amount))
    }
}

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

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

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

init() {
    self.totalSupply = 30.0
    self.tokenName = "Pinnie"

    let vault &lt;- create Vault(balance: self.totalSupply)
    self.account.save(&lt;-vault, to: /storage/MainVault)

    self.account.save(&lt;-create VaultMinter(), to: /storage/MainMinter)

    self.account.link&lt;&VaultMinter>(/private/Minter, target: /storage/MainMinter)
}
view rawPinnieToken.cdc hosted with  by GitHub

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

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

部署和铸造代币

我们需要更新项目中的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字段,然后我们添加sigAlogrithmhashAlgorithmchain属性。不管什么原因,这种格式对发送交易有效,而另一种格式对部署合约有效。

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

import PinnieToken from 0xf8d6e0586b0a20c7

transaction {
  prepare(acct: AuthAccount) {
    acct.link&lt;&PinnieToken.Vault{PinnieToken.Receiver, PinnieToken.Balance}>(/public/MainReceiver, target: /storage/MainVault)

    log("Public Receiver reference created!")
  }

  post {
    getAccount(0xf8d6e0586b0a20c7).getCapability&lt;&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&lt;&PinnieToken.Vault{PinnieToken.Receiver}>

    prepare(acct: AuthAccount) {
        self.mintingRef = acct.borrow&lt;&PinnieToken.VaultMinter>(from: /storage/MainMinter)
            ?? panic("Could not borrow a reference to the minter")

        let recipient = getAccount(0xf8d6e0586b0a20c7)

        self.receiver = recipient.getCapability&lt;&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&lt;&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 &lt;- PinnieToken.createEmptyVault()

        acct.save&lt;@PinnieToken.Vault>(&lt;-vaultA, to: /storage/MainVault)

    log("Empty Vault stored")

        let ReceiverRef = acct.link&lt;&PinnieToken.Vault{PinnieToken.Receiver, PinnieToken.Balance}>(/public/MainReceiver, target: /storage/MainVault)

    log("References created")
    }

    post {
        getAccount(NEW_ACCOUNT_ADDRESS).getCapability&lt;&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&lt;&PinnieToken.Vault>(from: /storage/MainVault)
        ?? panic("Could not borrow a reference to the owner's vault")

    self.temporaryVault &lt;- vaultRef.withdraw(amount: 10.0)
  }

  execute {
    let recipient = getAccount(NEW_ACCOUNT_ADDRESS)

    let receiverRef = recipient.getCapability(/public/MainReceiver)
                      .borrow&lt;&PinnieToken.Vault{PinnieToken.Receiver}>()
                      ?? panic("Could not borrow a reference to the receiver")

    receiverRef.deposit(from: &lt;-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&lt;&AnyResource{PinnieToken.Receiver}>

    init (vault: Capability&lt;&AnyResource{PinnieToken.Receiver}>) {
        self.forSale &lt;- {}
        self.ownerVault = vault
        self.prices = {}
    }

    pub fun withdraw(tokenID: UInt64): @PinataPartyContract.NFT {
        self.prices.remove(key: tokenID)
        let token &lt;- self.forSale.remove(key: tokenID) ?? panic("missing NFT")
        return &lt;-token
    }

    pub fun listForSale(token: @PinataPartyContract.NFT, price: UFix64) {
        let id = token.id

        self.prices[id] = price

        let oldToken &lt;- self.forSale[id] &lt;- 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: &lt;-buyTokens)

        let metadata = recipient.getMetadata(id: tokenID)
        recipient.deposit(token: &lt;-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&lt;&AnyResource{PinnieToken.Receiver}>): @SaleCollection {
  return &lt;- create SaleCollection(vault: ownerVault)
}

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

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

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&lt;&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 (
    &lt;div className="token-data">

    &lt;/div>
  );
};

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&lt;&{PinnieToken.Receiver}>(/public/MainReceiver)
        let sale &lt;- MarketplaceContract.createSaleCollection(ownerVault: receiver)

        let collectionRef = acct.borrow&lt;&PinataPartyContract.Collection>(from: /storage/NFTCollection)
            ?? panic("Could not borrow owner's nft collection reference")

        let token &lt;- collectionRef.withdraw(withdrawID: 1)

        sale.listForSale(token: &lt;-token, price: 10.0)

        acct.save(&lt;-sale, to: /storage/NFTSale)

        acct.link&lt;&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页面并刷新。在控制台中,你应该看到这样的东西。

它列出了指定账户地址的所有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&lt;&{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&lt;&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数组中。

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

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

setTokensToSell(marketplaceMetadata)

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

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

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

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

以下是我使用的样式,添加到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);
}

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

最后我们需要做的是连接那个 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&lt;&AnyResource{PinataPartyContract.NFTReceiver}>(from: /storage/NFTCollection)!
              let vaultRef = acct.borrow&lt;&PinnieToken.Vault>(from: /storage/MainVault)
                  ?? panic("Could not borrow owner's vault reference")
              self.temporaryVault &lt;- vaultRef.withdraw(amount: 10.0)
          }
          execute {
              let seller = getAccount(0xf8d6e0586b0a20c7)
              let saleRef = seller.getCapability&lt;&AnyResource{MarketplaceContract.SalePublic}>(/public/NFTSale)
                  .borrow()
                  ?? panic("Could not borrow seller's sale reference")
              saleRef.purchase(tokenID: tokenId, recipient: self.collectionRef, buyTokens: &lt;-self.temporaryVault)
          }
      }
    `,      
  ])
  await fcl.decode(txId);
  checkMarketplace();
}

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

&lt;button onClick={() => buyToken(1)} className="btn-primary">Buy Now&lt;/button>

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

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

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

结论

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

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

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

本翻译由 Cell Network 赞助支持。

  • 发表于 2021-03-30 15:01
  • 阅读 ( 3656 )
  • 学分 ( 350 )
  • 分类:NFT

评论