这是有关使用Flow和IPFS创建NFT(如NBA Top Shot)的三部分系列文章的第三部分。
第一部分:区块链研究实验室 | 如何使用Flow和IPFS创建NFT
第二部分:区块链研究实验室 | 如何使用Flow和IPFS显示NFT收藏品
在本系列的最后一部分中,我们将通过启用NFT来完成所有工作。正如我们所期望的那样,Flow拥有一些有关此概念的出色文档,但是我们将扩展这些文档,并使它们适合IPFS托管内容的范例。
配置
希望您一直关注前面的两个教程。如果是这样,您将拥有继续所需的所有入门代码,我们将简单地添加到该代码中。如果您还没有开始其他教程,那么您将会迷路。因此,一定要回过头来逐步学习这些教程。
现在,继续并打开您的项目。
创建合同
除了我们已经建立的市场之外,市场还需要一些其他的东西。让我们在这里列出这些东西:
支付机制(即可替代令牌)
代币转移能力
代币供应设置
由于Flow Emulator是Flow区块链的内存表示形式,因此您需要确保在执行此步骤之前执行先前的教程,并确保保持模拟器运行。假设您已完成此操作,让我们创建一个可替代的代币合约,该合约可用于购买NFT时进行支付。
明确地说,为我们将要创建的这些可替代令牌创建购买机制不在本教程的讨论范围之内。我们只是准备铸造令牌并将令牌转移到将要购买NFT的账户中。
在pinata-party本系列的第1部分中创建的目录中,进入该cadence/contracts文件夹并创建一个名为的新文件PinnieToken.cdc。这将是我们的可替代代币合同。我们将从定义空合同开始,如下所示:
pub contract PinnieToken {
}
主合同代码中的每个代码都将是其自己的Github要点,我将在最后提供完整的合同代码。我们要添加到该合同的第一部分是与我们的令牌和提供者资源关联的令牌发布变量。
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即将到来的参考。
该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。而且,我们要购买一些NFT。
现在,让我们看一下Vault资源实现的内容。您可以看到我们的Vault资源继承了Provider,Receiver和Balance资源接口,然后定义了两个函数: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)
}
当我们初始化合同时,我们需要设置总供应量。这可以是您选择的任何数字。就我们的示例而言,我们将其初始化为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返回如下所示:
本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
现在,我们准备继续前进。让我们铸造一些小兔子!为此,我们需要编写一个事务,代码如下:
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资源用于处理将新令牌存储到账户中的情况。
这只是一项测试,以确保我们可以铸造代币并将其存入我们自己的账户。很快,我们将创建一个新账户,即薄荷代币,并将其存入另一个账户。
从命令行运行事务,如下所示:
记住,我们使用仿真器账户部署了合同,因此,除非提供链接并允许其他账户铸造,否则仿真器账户必须进行铸造。
现在,让我们创建一个脚本来检查我们的Pinnie余额,以确保一切正常。在您的项目的scripts文件夹中,创建一个名为的文件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令牌的Balance资源的引用。我们在脚本末尾返回余额,以便将其打印在命令行上。
创建合同时,请记住,我们设置了30个代币的初始供应量。因此,当我们运行该MintPinnie交易时,我们应该已经铸造了另外30个令牌并将其存入仿真器账户。这意味着,如果一切顺利,此余额脚本应显示60个令牌。
我们可以使用以下命令运行脚本:
flow scripts execute scripts/CheckPinnieBalance.cdc
结果应该是这样的:
极好的!我们可以铸造代币。让我们确保可以铸造一些并将它们存入其他人的新账户中(真的,它仍然只是您,但我们可以假装)。
要创建一个新账户,您首先需要生成一个新的密钥对。为此,请运行以下命令:
flow keys generate
这将生成一个私钥和一个公钥。我们需要公钥来生成一个新账户,并且不久我们将使用私钥来更新我们的flow.json。因此,让我们现在创建一个新账户。运行以下命令:
flow accounts create --key YourNewPublicKey
这将创建一个交易,并且交易的结果将包括新的账户地址。创建新账户后,您应该已经收到了交易ID。复制该事务ID,然后运行以下命令:
flow transactions status YourTransactionId
此命令应导致如下所示:
列出的地址是新的账户地址。让我们用它来更新我们的flow.json文件。
在该文件内的accounts对象下,让我们创建对该账户的新引用。还记得以前的私钥吗?我们现在需要它。将您的账户对象设置为如下所示:
"accounts": {
"emulator-account": {
"address": "f8d6e0586b0a20c7",
"keys": "e5ca2b0946358223f0555206144fe4d74e65cbd58b0933c5232ce195b9058cdd"
},
"second-account": {
"address": "01cf0e2f2f715450",
"keys": "9bde7092cc0695c67f896e4375bffa0b5bf0a63ce562195a36f864ba7c3b09e3"
}
},
现在,我们有了第二个账户,可用于将Pinnie令牌发送至该账户。让我们看看它的外观。
发送可替代令牌
我们的主要账户(创建了Pinnie令牌的账户)目前有60个令牌。让我们看看是否可以将其中一些令牌发送到我们的第二个账户。
如果您记得较早,则每个账户都必须有一个空的保管库才能接受Pinnie令牌,并且还需要具有指向Pinnie Token合同上的资源的链接。让我们从创建一个空库开始。为此,我们需要进行新的交易。因此,请在您的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合同,我们正在调用public函数createEmptyVault,并且我们正在使用Receiver合同上的资源将其与新账户关联起来。
请注意,在本post节中,我们已经进行了检查。确保将其替换NEW_ACCOUNT_ADDRESS为之前创建的账户地址,并以开头0x。
让我们现在运行事务。在项目的根目录中,运行:
flow transactions send --code transactions/CreateEmptyPinnieVault.cdc --signer second-account
请注意,我们将定义signer为second-account。这是为了确保交易是由正确的账户执行的,而不是针对我们的原始账户执行的emulator-account。完成此操作后,我们现在可以链接到Pinnie令牌资源。运行以下命令:
flow transactions send --code transactions/LinkPinnie.cdc --signer second-account
所有这些都已设置好,以便我们可以将令牌从传输emulator-account到second-account。为此,您需要(您猜对了)另一笔交易。让我们现在写。
在您的transactions文件夹中,创建一个名为的文件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令牌保管库的临时引用。我们这样做是因为在处理可替代令牌时,所有事情都在库中进行。因此,我们需要从emulator-account的保管库中提取令牌,将其放入临时保管库,然后将该临时保管库发送给接收者(second-account)。
在第10行,您看到我们提取并发送到的金额second-account为10个令牌。看起来很公平。我们的朋友,second-account不要太贪心。
确保NEW_ACCOUNT_ADDRESS用second-account地址替换的值。以开头0x。完成后,让我们执行事务,运行:
flow transactions send --code transactions/TransferPinnieTokens.cdc --signer emulator-account
该signer需求是仿真账户,因为模拟器账户是唯一一个令牌现在。执行上述交易后,我们现在将拥有两个带有令牌的账户。让我们证明这一点。
打开CheckPinnieBalance脚本,将第3行上的账户地址替换为的地址second-account。同样,请确保在地址前面加上0x。保存该文件,然后像这样运行脚本:
flow scripts execute --code scripts/CheckPinnieBalance.cdc
您应该看到以下结果:
到此为止,您现在已经铸造了可以用作货币的可替代令牌,并且已将其中一些令牌转移给了另一个用户。现在,剩下的就是允许第二个账户从市场购买我们的NFT。
建立市场
我们将简单地更新本系列第二篇教程中的React代码,以充当我们的市场。我们将需要进行制作,以便在Pinnie代币中的价格旁边显示NFT。我们将需要一个允许用户购买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中删除了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函数中执行此操作,并仅使用空值和所有者的保管库资源进行初始化。
接下来是该资源的实质。我们正在定义控制我们所有市场行为的功能。这些功能是:
提取
listForSale
changePrice
购买
idPrice
getIDs
破坏
如果您还记得的话,我们以前只公开公开了其中三个功能。这意味着,without,listForSale,changePrice和destroy仅对所列出的NFT所有者可用,这是有道理的。例如,我们不希望任何人能够更改NFT的价格。
我们Marketplace合同的最后一部分是createSaleCollection功能,这允许将集合作为资源添加到账户。看起来像这样,位于SaleCollection资源之后:
pub fun createSaleCollection(ownerVault: Capability<&AnyResource{PinnieToken.Receiver}>): @SaleCollection {
return <- 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<&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,则应该在控制台中看到“无NFT可供出售”。
为了简洁起见,我们将通过Flow CLI工具列出要出售的铸造NFT。但是,您可以通过UI扩展本教程。在您的root 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接收器功能,因为我们正在接受PinnieTokens中的付款。我们还需要访问createSaleCollectionMarketplaceContract上的功能。然后,我们需要参考要出售的NFT。我们撤回该NFT,将其出售,售价为10.0 PinnieTokens,然后将其保存到NFTSale存储路径中。
如果运行以下命令,则应该可以成功列出之前创建的NFT。
flow transactions execute --code transactions/ListTokenForSale.cdc
现在,返回您的React App页面并刷新。在控制台中,您应该看到类似以下内容的内容:
这是指定要出售的账户地址的令牌ID数组。这意味着,我们知道要查找和获取其元数据的ID。在我们的简单示例中,仅列出了一个令牌,这是我们创建的也是唯一的令牌,因此其tokenID为1。
在将代码添加到React App之前,将以下导入添加到TokenData.js文件顶部:
import * as t from "@onflow/types"
这使我们可以将参数传递给使用发送的脚本fcl。
好的,现在我们可以使用我们的tokenID数组,并使用以前必须获取令牌元数据的一些代码。在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数组。
现在,在控制台中,您应该会看到类似以下内容的内容:
这很棒!现在,我们可以渲染列出的代币并显示价格。现在就开始吧。
在显示marketplaceMetadata阵列的console.log语句下,添加以下内容:
setTokensToSell(marketplaceMetadata)
您还需要在TokenData主函数声明的开始下方添加以下内容:
const TokenData = () => {
const [tokensToSell, setTokensToSell] = useState([])
}
有了这些东西,您就可以渲染您的市场。在return语句中,添加以下内容:
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);
}
您的应用现在应该看起来像这样:
我们需要做的最后一件事是连接“立即购买”按钮,并允许非NFT所有者的人购买NFT。
购买NFT
通常,您需要通过远程发现节点终结点进行钱包发现和事务处理。我们实际上在本系列的第二部分中的React应用程序中进行了设置。但是,我们正在使用本地Flow仿真器。因此,我们需要运行本地开发者钱包,然后需要更新React应用程序的环境变量。
让我们进行设置。首先,克隆本地开发者钱包。从pinata-party项目的根目录运行:
git clone git@github.com:onflow / fcl-dev-wallet.git
完成后,转到文件夹:
cd fcl-dev-wallet
现在,我们需要复制示例环境文件并创建开发钱包将使用的本地环境文件:
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应用程序。
下一步是连接前端“立即购买”按钮以实际发送交易以购买令牌。打开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();
}
现在我们只需要onClick为“立即购买”按钮添加一个处理程序即可。这就像将按钮更新为如下所示一样简单:
<button onClick={() => buyToken(1)} className="btn-primary">Buy Now</button>
我们在这里对tokenID进行了硬编码,但是您可以轻松地从我们执行的早期脚本中获取它。
现在,当您进入React应用程序并单击“立即购买”按钮时,您应该会看到类似以下的屏幕:
正如标题中所说的那样,fcl-dev-wallet处于Alpha状态,所以事实是,事务的执行可能会成功也可能不会成功。但是,进一步说明您的应用程序可以正常运行,而fcl库也可以正常运行。
作者:链三丰,来源:区块链研究实验室
郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。
郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。