How to create an NFT like NBA TOP SHOT on Flow and ipfs
FlowTimes福洛时代
2021-03-16 12:28
本文约9781字,阅读全文需要约39分钟
We'll build a very basic example and then back up the NFT's metadata on IPFS.

With the non-fungible token (NFT) marketreach climax, looking back at relatively early NFTs and recallingCryptoKittiesThe challenges are fun. This is byDapper LabsThe first large-scale use case built by the team also put Ethereum under traffic pressure for the first time.

andRaribleOpenSeaFoundationandSorarePlatforms like this started to emerge. Millions of dollars of funds flow through these platforms every month, but most of this happens on the Ethereum blockchain. However, once the team at Dapper Labs had experience developing CryptoKitties, they set out toCreate a new public chain, the public chain will be universal, but also very suitable for the development of NFT. The goal of this is to solve many of the problems encountered with NFTs on Ethereum, while providing a better experience for developers and collectors in the space. Their new blockchain, Flow, has already proven itself capable of winning partnerships with some big-name brands. For exampleNBA, UFC, and even Dr. Seuss are also on the flow.

We recently wrote aboutCreate NFTs with built-in asset support on IPFSarticle, and we discuss the issue of liability in the NFT space and how we think IPFS can help. Now it's time to discuss how to create nfts on IPFS powered flows. One of the major early applications of the Flow blockchain isNBA Top Shot. We'll build a very basic example and then back up the NFT's metadata on IPFS.

because we likepiñatas, rather than an NBA highlight video, so we'll create a video of tradable piñatas being smashed at a party.

This is a three-part tutorial:
1. Create a contract and mint tokens
2. Create an app to view the NFT created by this contract
3. Create a market to transfer NFT to others, and also transfer the underlying assets of this NFT on IPFS

Let's start the first tutorial.

configuration

We need to install Flow CLI. Flow's documentation has some good installation instructions, but I'll reproduce them here:

Apple system
brew install flow-cli

Linux

sh -ci “$(curl -fsSL https://storage.googleapis.com/flow-cli/install.sh)"

Windows

iex “& { $(irm ‘https://storage.googleapis.com/flow-cli/install.ps1') }”

We will store asset files on IPFS. To simplify things, we can usePinata. You can sign up for one herefree account, and get the API key here. In the second article of this tutorial, we will use theAPI, but in this article, we will use the Pinata website.

We also need to install NodeJS and a text editor to help highlight the Flow smart contract code (started withCadencelanguage) syntax. You can install Node here. Visual Studio Code hasSupport for Cadence extensions

Let's create a directory to start our project.

mkdir pinata-party

Change into that directory and initialize a new flow project:

cd pinata-party
flow project init

Now, open the project in your favorite code editor (again, if you use Visual Studio Code, install the Cadence extension) and start developing.

You'll see a flow.json file, which we'll be using shortly. First, create a folder called cadence. Inside that folder, add another folder called contracts. Finally, create a file called PinataPartyContract.cdc in the contracts folder.

Before moving forward, it's important to point out that everything we do with the Flow blockchain from now on will be done on the emulator. However, deploying the project to testnet or mainnet is as easy as updating the configuration settings in the flow.json file. Now let's set up this file for the emulator environment, and then we can start writing contracts.

Update the contracts in flow.json to look like this:

"contracts": {
    "PinataPartyContract": "./cadence/contracts/PinataPartyContract.cdc"
}

Then, update the deployments in flow.json as follows:

"deployments": {
    "emulator": {
         "emulator-account": ["PinataPartyContract"]
    }
}

This is actually telling Flow CLI to use the emulator to deploy our contract, it also references the account (on the emulator) and the contract we are about to write.

contract

contract

Flow provides an excellent tutorial on creating NFT contracts. This is a good reference point, but as Flowas pointed out, they haven't addressed the NFT metadata issue yet. They want to store metadata on-chain. That's a good idea, and they'll definitely come up with a logical approach. However, we now want to create some tokens with metadata, and we want media files associated with the NFT. Metadata is only one component. We also need to indicate which media file the token ultimately represents.

If you are familiar with NFTs on the Ethereum blockchain, you probably know that many of the assets returned by these tokens are stored inTraditional data storage and cloud service providersmiddle. This is ok as long as they don't go down. We've written in the past about content finding and storing it on traditional cloud platforms vs. blockchain. It boils down to two points:

  • Assets should be verifiable

  • Storage transfer should be easy

IPFSBoth aspects are taken into account. Pinata is then layered in an easy way to store content long-term on IPFS. That's exactly what we need for media that we want to support NFTs, right? We want to make sure that we can prove ownership (NFT), provide data about the NFT (NFT), and make sure we have control over the underlying asset (IPFS) - (media file or whatever), not some replica. With all of this in mind, let's write a contract that creates an NFT, associates metadata with the NFT, and ensures that the metadata points to the underlying asset stored on IPFS.

Open PinataPartyContract.cdc and let's get to work.

pub contract PinataPartyContract {
 pub resource NFT {
   pub let id: UInt64
   init(initID: UInt64) {
     self.id = initID
   }
 }
}

The first step is to define our contract. We'll add more to this, but first we define the PinataPartyContract and create a resource inside it. Resources are items stored in user accounts and can be accessed through access controls. In this case, the NFT resource is ultimately owned by owning the thing used to represent the NFT. NFTs must be uniquely identifiable. The id attribute allows us to identify the token.

Next, we need to create a resource interface that will be used to define which functionality is available to others (i.e. people who are not the owner of the contract):

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

Put it below the NFT resource code. The NFTReceiver interface means that whoever we define as having access to the resource will be able to call the following methods:

  • deposit

  • getIDs

  • idExists

  • getMetadata

Next, we need to define our token collection interface. Think of it as a wallet that holds all of a user's NFTs.

pub resource Collection: NFTReceiver {
   pub var ownedNFTs: UInt64: NFT}
   pub var metadataObjs: {UInt64: { String : String }}

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

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

       return <-token    }

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

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

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

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

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

   destroy() {
       destroy self.ownedNFTs    }}

There is a lot going on in this resource. First, we have a variable called ownedNFTs . this is very simple. It keeps track of all NFTs owned by the user in that contract.

Next, we have a variable called metadataObjs. This is a bit unique in that we are extending the Flow NFT contract functionality to store metadata mappings for each NFT. This variable maps the token ID to its associated metadata, which means we need to set the token ID before we can set it.

Then, we initialize the variables. This is required for variables defined in resources in Flow.

Finally, we have all the features available for NFT collection resources. Note that not all of these features are available. If you recall, we defined the functionality available to anyone earlier in the NFTReceiver resource interface.

I do want to point out the deposit function. Just as we extended the default Flow NFT contract to include the metadataObjs map, we are also extending the default deposit function to take an additional parameter metadata. Why are we doing this here? We need to ensure that only the minter of the token can add that metadata to the token. To maintain privacy, we restrict the initial addition of metadata to the minting execution.

Our contract code is almost complete. So, below the Collection resource, add the following:

pub fun createEmptyCollection(): @Collection {
   return <- create Collection()}pub resource NFTMinter {
   pub var idCount: UInt64

   init() {
       self.idCount = 1
   }

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

       self.idCount = self.idCount + 1 as UInt64        return <-newNFT    }}

First, we have a function that, when called, creates an empty NFT collection. This way, users interacting with our contract for the first time will have a storage location created into the Collection of resources we define.

After that, we create another resource. This is important because without it we would not be able to mint tokens. The NFTMinter resource includes an idCount which is incremented to ensure we never have duplicate ids on our NFTs. It also has the functionality to actually create our NFT.

Below the NFTMinter resource, add the main contract initializer:

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

This initialization function is only called when the contract is deployed. It does three things:
1. Create an empty collection for the deployer of the collection, so that the owner of the contract can create the NFT of the contract and own the NFT.
2.Collection Reference NFTReceiver The interface we created at the beginning, this resource is published in a public location. This is how we tell the contract that anyone can call the functions defined on NFTReceiver .
3. The NFTMinter resource is saved in the account storage contract creator. This means that only the creator of the contract can mint tokens.

complete contractcan be found here.

Now that we have our contract ready, let's deploy it, shall we? Well, we should probably be atFlow PlaygroundTest it on . Go there and click on the first account in the left sidebar. Replace all the code in the example contract with our contract code and click deploy. If all went well, you should see a log like this in the log window at the bottom of the screen:

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

Now, we are ready to deploy the contract to the emulator running locally. At the command line, run the following command:
flow project start\-emulator

Now, with the emulator running and the flow.json file properly configured, we can deploy the contract. Just run the following command:
flow project deploy

If all goes well, you should see output similar to the following:
Deploy 1 contract for account: emulator account
Deploying 1 contracts for accounts: emulator-account
PinataPartyContract -> 0xf8d6e0586b0a20c7
Minting NFTs

Minting NFTs

In the second article of this tutorial, we will work on making the casting process more user-friendly through the application and user interface. To illustrate and show how metadata will work with NFTs on Flow, we will use Cadence scripts and the command line.
Let's create a new directory at the root of the pinata-party project and call it transactions. After creating the folder, create a new file named MintPinataParty.cdc inside it.

In order to write a transaction, we need to reference a file in the metadata provided to the NFT. To do this, we will upload the file to IPFS via Pinata. In this tutorial, since our NFT is focused on tradable videos of smashed piñatas at parties, we'll upload a video of a child hitting a piñata at a birthday party. You can upload any video file you want. You can really upload any asset file and associate it with an NFT, but the second article in this tutorial series will look forward to video content. Once the video file is ready to play,Please upload here. After uploading a file, you'll be given an IPFS hash (often called a Content Identifier or CID). Copy this hash as we will use it during the minting process.

Now, add the following to the MintPinataParty.cdc file:

import PinataPartyContract from 0xf8d6e0586b0a20c7transaction {
 let receiverRef: &{PinataPartyContract.NFTReceiver}
 let minterRef: &PinataPartyContract.NFTMinter

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

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

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

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

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

This is a pretty simple affair, thanks in no small part to the work Flow has done to make things easy, but let's walk through it. First, you'll notice the import statement at the top. If you recall, when we deploy a contract, we receive an account. That's what we need to refer to. Therefore, replace 0xf8d6e0586b0a20c7 with the account address in your deployment.

Next, we define transactions. Everything that happens here is related to the transactions we plan to execute.

The first thing we do in our transaction is define two reference variables receiverRef and minterRef. In this case, we are both the receiver of the NFT and the minter of the NFT. These two variables refer to the resources we created in the contract. If the person performing the transaction does not have access to the resource, the transaction will fail.

Next, we have a prepare function. This function takes the account information of the person trying to perform the transaction and does some verification. We try to "borrow" the NFTMinter we defined and the function NFTReceiver available on both resources. If the person performing the transaction does not have access to those resources, then things will fail.

Finally, we have our execute function. This function is where we build metadata for the NFT, create the NFT, then associate the metadata before depositing the NFT into our account. If you noticed, I created a metadata variable. In that variable, I added some information about the token. Since our token represents the event of smashing a pizza at a party, and since we're trying to replicate most of what you see in NBA Top Shot, I defined some statistics in the metadata. The child swings the stick to hit the piñata speed, swing angle and grade. I'm just playing around with these stats. However, you will enter any information that makes sense for your token in a similar manner.

You'll notice that I've also defined an attribute in the uri metadata. This will point to the IPFS hash hosting the asset file associated with the NFT. In this case, it's the actual video of the Piñata being hit. You can replace the hash with the hash you received after uploading the file earlier.

We prefix the hash with ipfs:// . This is the proper reference for files on IPFS and can be used with both desktop clients and browser extensions for IPFS. brave also provides support for it, we can also paste it directly intoIn the Brave browser

We call the mintNFT function that creates the token. We then have to call the deposit function to deposit it into our account. This is also where we pass metadata. Remember, we defined a variable association in the deposit function that adds metadata to the associated token ID.

Now, we are almost ready to send the transaction and create the NFT. But first, we need to prepare our account. From the command line in the project's root folder, let's create a new private key for signing. Run the following command:

flow keys generate

This will give you a public key and a private key. Always protect your private key

We will need the private key to sign the transaction, so we can paste that into our flow.json file. We also need to specify the signature algorithm. This is the accounts object in the file flow.json should now look like this:

“ accounts”:{
 “ emulator-account”:{
"address": "your account address",
"privateKey": "your private key",
"chain": "Stream Emulator",
    “ sigAlgorithm”:“ ECDSA_P256”,
    “ hashAlgorithm”:“ SHA3_256”
 }
},

If you plan to store any of this project on github or any remote git repository, make sure not to include the private key. You can add flow.json in .gitignore. Even though we're only using the local emulator, it's a good practice to protect your keys.

Now that we've updated, we can send transactions. Doing so is as easy as running the following command:
flow transactions send --code ./transactions/MintPinataParty.cdc --signer emulator-account

We reference the transaction file and signer account we wrote from flow.json. If all goes well, you should see output similar to the following:

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

Now, the last thing we need to do is verify that the token is in our account and get the metadata. To do so, we'll write a very simple script and call it from the command line.

In the root of the project, create a new folder called scripts. Create a file named CheckTokenMetadata.cdc inside it. In that file, add the following:

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

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

   return receiverRef.getMetadata(id: 1)}

This script can be thought of in a manner similar to a read-only method on an Ethereum smart contract. They are free, just return the data from the contract.

In the script, we import the contract from the deployed address. Then, we define a main function (this is the name of the function needed to run the script). Inside this function, we define three variables:

  • nftOwner: This is just the account that owns the NFT. We minted the NFT from an account that also deployed the contract, so in our example the two addresses are the same. Depending on future contract design, this may not always be true.

  • capability: We need to "borrow" from the deployed contract. Remember that these functions are access-controlled, so if a function is not available for the address trying to borrow it, the script will fail. We are borrowing from the NFTReceiver resource.

  • receiverRef: This variable just leverages our ability and tells the script to borrow from the deployed contract.

Now, we can call functions (available functions). In this case, we want to make sure that the address in question has actually received the NFT we minted, and then we want to look at the metadata associated with the token.

Let's run our script and see what we get. Run the following commands on the command line:
flow scripts execute --code ./scripts/CheckTokenMetadata.cdc

For metadata output, you should see output similar to the following:
{"name": "The Big Swing", "swing_velocity": "29", "swing_angle": "45", "rating": "5", "uri": "ipfs://QmRZdc3mAMXpv6Akz9Ekp1y4vDSjazTx2dCQRkxVy1yUj6"}

congratulations! You have successfully created a Flow smart contract, minted a token and the metadata associated with that token, and stored the token's underlying digital asset on IPFS. Not bad for the first part of the tutorial.

Next, we have a tutorial on building a front-end React app, by getting the metadata and parsing that metadata, you can display the NFT

FlowTimes福洛时代
作者文库