Neste tutorial, aprenderemos como construir um site para coleta de itens colecionáveis digitais (ou NFTs) no blockchain Flow. Usaremos a linguagem de contrato inteligente Cadence junto com React para fazer tudo acontecer. Também aprenderemos sobre o Flow, suas vantagens e as ferramentas divertidas que podemos usar.
Ao final deste artigo, você terá as ferramentas e o conhecimento necessários para criar seu próprio aplicativo descentralizado no blockchain Flow.
Vamos mergulhar de cabeça!
Estamos construindo um aplicativo para colecionáveis digitais. Cada colecionável é um Token Não Fungível (NFT).
Para fazer tudo isso funcionar, usaremos o NonFungibleToken Standard do Flow, que é um conjunto de regras que nos ajuda a gerenciar esses itens digitais especiais (semelhante ao ERC-721 no Ethereum).
Antes de começar, certifique-se de instalar o Flow CLI em seu sistema. Caso ainda não tenha feito isso, siga estas instruções de instalação .
Se você estiver pronto para iniciar seu projeto, primeiro digite o comando flow setup.
Este comando faz mágica nos bastidores para estabelecer a base do seu projeto. Ele cria um sistema de pastas e configura um arquivo chamado flow.json para configurar seu projeto, garantindo que tudo esteja organizado e pronto para uso!
O projeto conterá uma pasta cadence
e um arquivo flow.json
. (Um arquivo flow.json é um arquivo de configuração do seu projeto, mantido automaticamente.)
A pasta Cadence contém o seguinte:
Siga as etapas abaixo para usar o Flow NFT Standard.
Primeiro, vá para a pasta flow-collectibles-portal
e encontre a pasta cadence
. Em seguida, abra a pasta contracts
. Crie um novo arquivo e nomeie-o como NonFungibleToken.cdc
.
Agora, abra o link chamado NonFungibleToken que contém o padrão NFT. Copie todo o conteúdo desse arquivo e cole-o no novo arquivo que você acabou de criar ("NonFungibleToken.cdc").
É isso! Você configurou com sucesso os padrões para seu projeto.
Agora, vamos escrever algum código!
No entanto, antes de mergulharmos na codificação, é importante que os desenvolvedores estabeleçam um modelo mental de como estruturar seu código.
No nível superior, nossa base de código consiste em três componentes principais:
NFT: Cada colecionável é representado como um NFT.
Coleção: Uma coleção refere-se a um grupo de NFTs de propriedade de um usuário específico.
Funções e Variáveis Globais: São funções e variáveis definidas em nível global para o contrato inteligente e não estão associadas a nenhum recurso específico.
Crie um novo arquivo chamado Collectibles.cdc
dentro de cadence/contracts
. É aqui que escreveremos o código.
Estrutura do Contrato
import NonFungibleToken from "./NonFungibleToken.cdc" pub contract Collectibles: NonFungibleToken{ pub var totalSupply: UInt64 // other code will come here init(){ self.totalSupply = 0 } }
Vamos dividir o código linha por linha:
Primeiro, precisaremos padronizar que estamos construindo um NFT, incluindo o chamado “NonFungibleToken”. Este é um padrão NFT desenvolvido pela Flow que define o seguinte conjunto de funcionalidades que devem ser incluídas em cada contrato inteligente NFT.
Após a importação, vamos criar nosso contrato. Para fazer isso, usamos pub contract [contract name]
. Use a mesma sintaxe sempre que criar um novo contrato. Você pode preencher o contract name
com o nome que desejar. No nosso caso, vamos chamá-lo Collectibles
.
Em seguida, queremos ter certeza de que nosso contrato segue um determinado conjunto de funcionalidades e regras do NonFungibleToken. Para fazer isso, adicionamos uma interface NonFungibleToken com a ajuda de `:`.
Assim ( `pub contract Collectibles: NonFungibleToken{}`
)
Cada contrato DEVE ter a função init()
. É chamado quando o contrato é implantado inicialmente. Isso é semelhante ao que o Solidity chama de Construtor.
Agora, vamos criar uma variável global chamada totalSupply
com tipo de dados UInt64
. Esta variável acompanhará o total de seus itens colecionáveis.
Agora, inicialize totalSupply
com valor 0
.
É isso! Estabelecemos a base para o nosso contrato Collectibles
. Agora podemos começar a adicionar mais recursos e funcionalidades para torná-lo ainda mais interessante.
Antes de prosseguir, verifique o trecho de código para entender como definimos variáveis no Cadence:
Adicione o seguinte código ao seu contrato inteligente:
import NonFungibleToken from "./NonFungibleToken.cdc" pub contract Collectibles: NonFungibleToken{ // above code… pub resource NFT: NonFungibleToken.INFT{ pub let id: UInt64 pub var name: String pub var image: String init(_id:UInt64, _name:String, _image:String){ self.id = _id self.name = _name self.image = _image } } // init()... }
Como você viu antes, o contrato implementa a interface padrão NFT, representada pelo pub contract Collectibles: NonFungibleToken
. Da mesma forma, os recursos também podem implementar várias interfaces de recursos.
Então, vamos adicionar a interface NonFungibleToken.INFT
ao recurso NFT, que exige a existência de uma propriedade pública chamada id dentro do recurso.
Aqui estão as variáveis que usaremos no recurso NFT:
id:
Mantém o ID do NFTname:
Nome do NFT.image:
URL da imagem do NFT.
Depois de definir a variável, certifique-se de inicializá-la na função init()
.
Vamos seguir em frente e criar outro recurso chamado Collection Resource
.
Primeiro, você precisa entender como funcionam Collection Resources
.
Se você precisasse armazenar um arquivo de música e diversas fotos em seu laptop, o que você faria?
Normalmente, você navegaria para uma unidade local (digamos, seu D-Drive) e criaria uma pasta music
e uma pasta photos
. Em seguida, você copiaria e colaria os arquivos de música e fotos nas pastas de destino.
Da mesma forma, é assim que funcionam seus itens colecionáveis digitais no Flow.
Imagine seu laptop como uma Flow Blockchain Account
, seu D-Drive como Account Storage
e uma pasta como uma Collection
.
Assim, ao interagir com qualquer projeto de compra de NFTs, o projeto cria sua collection
no account storage
, semelhante à criação de uma pasta no seu D-Drive. Ao interagir com 10 projetos NFT diferentes, você terá 10 coleções diferentes em sua conta.
É como ter um espaço pessoal para armazenar e organizar seus tesouros digitais exclusivos!
import NonFungibleToken from "./NonFungibleToken.cdc" pub contract Collectibles: NonFungibleToken{ //Above code NFT Resource… // Collection Resource pub resource Collection{ } // Below code… }
Cada collection
possui uma variável ownedNFTs
para armazenar os NFT Resources
.
pub resource Collection { pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} init(){ self.ownedNFTs <- {} } }
Interfaces de recursos
Uma interface resource
no Flow é semelhante às interfaces de outras linguagens de programação. Ele fica sobre um recurso e garante que o recurso que o implementa tenha a funcionalidade necessária conforme definido pela interface.
Também pode ser usado para restringir o acesso a todo o recurso e ser mais restritivo em termos de modificadores de acesso do que o próprio recurso.
No padrão NonFungibleToken
, existem várias interfaces de recursos como INFT
, Provider
, Receiver
e CollectionPublic
.
Cada uma dessas interfaces possui funções e campos específicos que precisam ser implementados pelo recurso que as utiliza.
Neste contrato, usaremos essas três interfaces do NonFungibleToken: Provider
, Receiver
e CollectionPublic
. Essas interfaces definem funções como deposit
, withdraw
, borrowNFT
e getIDs
. Explicaremos cada um deles em detalhes à medida que avançamos.
Também adicionaremos alguns eventos que emitiremos a partir dessas funções, bem como declararemos algumas variáveis que usaremos mais adiante no tutorial.
pub contract Collectibles:NonFungibleToken{ // rest of the code… pub event ContractInitialized() pub event Withdraw(id: UInt64, from: Address?) pub event Deposit(id: UInt64, to: Address?) pub let CollectionStoragePath: StoragePath pub let CollectionPublicPath: PublicPath pub resource interface CollectionPublic{ pub fun deposit(token: @NonFungibleToken.NFT) pub fun getIDs(): [UInt64] pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT } pub resource Collection: CollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic{ pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} init(){ self.ownedNFTs <- {} } } }
Retirar
Agora, vamos criar a função withdraw()
exigida pela interface.
pub resource Collection: CollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic{ // other code pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT") emit Withdraw(id: token.id, from: self.owner?.address) return <- token } init()... }
Com a ajuda desta função, você pode retirar o recurso NFT da coleção. Se isso:
O chamador pode então usar esse recurso e salvá-lo no armazenamento de sua conta.
Depósito
Agora é hora da função deposit()
exigida por NonFungibleToken.Receiver
.
pub resource Collection: CollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic{ // other code pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT") emit Withdraw(id: token.id, from: self.owner?.address) return <- token } pub fun deposit(token: @NonFungibleToken.NFT) { let id = token.id let oldToken <- self.ownedNFTs[id] <-token destroy oldToken emit Deposit(id: id, to: self.owner?.address) } init()... }
Emprestar e obter ID
Agora, vamos nos concentrar nas duas funções exigidas por NonFungibleToken.CollectionPublic: borrowNFT()
e getID()
.
pub resource Collection: CollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic{ // other code pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT") emit Withdraw(id: token.id, from: self.owner?.address) return <- token } pub fun deposit(token: @NonFungibleToken.NFT) { let id = token.id let oldToken <- self.ownedNFTs[id] <-token destroy oldToken emit Deposit(id: id, to: self.owner?.address) } pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { if self.ownedNFTs[id] != nil { return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)! } panic("NFT not found in collection.") } pub fun getIDs(): [UInt64]{ return self.ownedNFTs.keys } init()... }
Destruidor
A última coisa que precisamos para o Collection Resource é um destruidor.
destroy (){ destroy self.ownedNFTs }
Como o recurso Collection contém outros recursos (recursos NFT), precisamos especificar um destruidor. Um destruidor é executado quando o objeto é destruído. Isso garante que os recursos não fiquem “sem abrigo” quando seu recurso pai for destruído. Não precisamos de um destruidor para o recurso NFT, pois ele não contém nenhum outro recurso.
Vejamos o código-fonte completo do recurso de coleção:
import NonFungibleToken from "./NonFungibleToken.cdc" pub contract Collectibles: NonFungibleToken{ pub var totalSupply: UInt64 pub resource NFT: NonFungibleToken.INFT{ pub let id: UInt64 pub var name: String pub var image: String init(_id:UInt64, _name:String, _image:String){ self.id = _id self.name = _name self.image = _image } } pub resource interface CollectionPublic{ pub fun deposit(token: @NonFungibleToken.NFT) pub fun getIDs(): [UInt64] pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT } pub event ContractInitialized() pub event Withdraw(id: UInt64, from: Address?) pub event Deposit(id: UInt64, to: Address?) pub let CollectionStoragePath: StoragePath pub let CollectionPublicPath: PublicPath pub resource Collection: CollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic{ pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} init(){ self.ownedNFTs <- {} } destroy (){ destroy self.ownedNFTs } pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT") emit Withdraw(id: token.id, from: self.owner?.address) return <- token } pub fun deposit(token: @NonFungibleToken.NFT) { let id = token.id let oldToken <- self.ownedNFTs[id] <-token destroy oldToken emit Deposit(id: id, to: self.owner?.address) } pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { if self.ownedNFTs[id] != nil { return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)! } panic("NFT not found in collection.") } pub fun getIDs(): [UInt64]{ return self.ownedNFTs.keys } } init(){ self.CollectionPublicPath = /public/NFTCollection self.CollectionStoragePath = /storage/NFTCollection self.totalSupply = 0 emit ContractInitialized() } }
Agora terminamos todos os recursos. A seguir, veremos a função global.
Funções Globais são funções definidas no nível global do contrato inteligente, o que significa que não fazem parte de nenhum recurso. Eles são acessíveis e chamados pelo público e expõem ao público a funcionalidade central do contrato inteligente.
createEmptyCollection : esta função inicializa um Collectibles.Collection
vazio no armazenamento da conta do chamador.
checkCollection : esta função útil ajuda você a descobrir se sua conta já possui ou não um recurso collection
.
mintNFT : Essa função é super legal porque permite que qualquer pessoa crie um NFT.
// pub resource Collection… pub fun createEmptyCollection(): @Collection{ return <- create Collection() } pub fun checkCollection(_addr: Address): Bool{ return getAccount(_addr) .capabilities.get<&{Collectibles.CollectionPublic}> (Collectibles.CollectionPublicPath)! .check() } pub fun mintNFT(name:String, image:String): @NFT{ Collectibles.totalSupply = Collectibles.totalSupply + 1 let nftId = Collectibles.totalSupply var newNFT <- create NFT(_id:nftId, _name:name, _image:image) return <- newNFT } init()...
E agora, FINALMENTE, com tudo pronto, terminamos de escrever nosso contrato inteligente. Dê uma olhada no código final aqui .
Agora, vamos ver como um usuário interage com contratos inteligentes implantados no blockchain Flow.
Existem duas etapas para interagir com o blockchain Flow:
As transações são dados assinados criptograficamente que contêm um conjunto de instruções que interagem com o contrato inteligente para atualizar o estado do Flow. Em termos simples, é como uma chamada de função que altera os dados no blockchain. As transações geralmente envolvem algum custo, que pode variar dependendo do blockchain em que você está.
Uma transação inclui várias fases opcionais: prepare
, pre
, execute
e post
fase.
Você pode ler mais sobre isso no documento de referência da Cadence sobre transações . Cada fase tem um propósito; as duas fases mais importantes são prepare
e execute
.
Prepare Phase
: Esta fase é utilizada para acessar dados e informações dentro da conta do assinante (permitido pelo tipo AuthAccount).
Execute Phase
: esta fase é usada para executar ações.
Agora, vamos criar uma transação para nosso projeto.
Siga as etapas abaixo para criar uma transação na pasta do seu projeto.
Primeiro, vá para a pasta do projeto e abra a pasta cadence
. Dentro dela, abra a pasta transaction
e crie um novo arquivo com os nomes Create_Collection.cdc
e mint_nft.cdc
import Collectibles from "../contracts/Collectibles.cdc" transaction { prepare(signer: AuthAccount) { if signer.borrow<&Collectibles.Collection>(from: Collectibles.CollectionStoragePath) == nil { let collection <- Collectibles.createEmptyCollection() signer.save(<-collection, to: Collectibles.CollectionStoragePath) let cap = signer.capabilities.storage.issue<&{Collectibles.CollectionPublic}>(Collectibles.CollectionStoragePath) signer.capabilities.publish( cap, at: Collectibles.CollectionPublicPath) } } }
Vamos dividir esse código linha por linha:
Esta transação interage com o contrato inteligente de Colecionáveis. Em seguida, ele verifica se o remetente (signatário) possui um recurso Collection armazenado em sua conta, emprestando uma referência ao recurso Collection do caminho de armazenamento especificado Collectibles.CollectionStoragePath
. Se a referência for nula, significa que o signatário ainda não possui uma coleção.
Se o signatário não tiver uma coleção, ele criará uma coleção vazia chamando a função createEmptyCollection()
.
Depois de criar a coleção vazia, coloque-a na conta do signatário no caminho de armazenamento especificado Collectibles.CollectionStoragePath
.
Isso estabelece um link entre a conta do signatário e a coleção recém-criada usando link()
.
import NonFungibleToken from "../contracts/NonFungibleToken.cdc" import Collectibles from "../contracts/Collectibles.cdc" transaction(name:String, image:String){ let receiverCollectionRef: &{NonFungibleToken.CollectionPublic} prepare(signer:AuthAccount){ self.receiverCollectionRef = signer.borrow<&Collectibles.Collection>(from: Collectibles.CollectionStoragePath) ?? panic("could not borrow Collection reference") } execute{ let nft <- Collectibles.mintNFT(name:name, image:image) self.receiverCollectionRef.deposit(token: <-nft) } }
Vamos dividir esse código linha por linha:
Primeiro importamos o Collectibles contract
NonFungibleToken
e Collectibles.
transaction(name: String, image: String)
Esta linha define uma nova transação. São necessários dois argumentos, nome e imagem, ambos do tipo String. Esses argumentos são usados para passar o nome e a imagem do NFT que está sendo cunhado.
let receiverCollectionRef: &{NonFungibleToken.CollectionPublic}
Esta linha declara uma nova variável receiverCollectionRef.
É uma referência a uma coleção pública de NFTs do tipo NonFungibleToken.CollectionPublic
. Esta referência será utilizada para interagir com a coleção onde depositaremos o NFT recém-cunhado.
prepare(signer: AuthAccount)
Esta linha inicia o bloco de preparação, que é executado antes da transação. É necessário um assinante de argumento do tipo AuthAccount
. AuthAccount
representa a conta do signatário da transação.
Ele empresta uma referência ao Collectibles.Collection
do armazenamento do signatário dentro do bloco de preparação. Ele usa a função de empréstimo para acessar a referência à coleção e armazená-la na variável receiverCollectionRef
.
Se a referência não for encontrada (se a coleção não existir no armazenamento do signatário, por exemplo), será gerada a mensagem de erro “não foi possível emprestar a referência da coleção”.
O bloco execute
contém a lógica de execução principal da transação. O código dentro deste bloco será executado após a conclusão bem-sucedida do bloco prepare
.
nft <- Collectibles.mintNFT(_name: name, image: image)
Dentro do bloco execute
, esta linha chama a função mintNFT
do contrato Collectibles
com os argumentos de nome e imagem fornecidos. Espera-se que esta função crie um novo NFT com o nome e imagem fornecidos. O símbolo <-
indica que o NFT está sendo recebido como um objeto que pode ser movido (um recurso).
self.receiverCollectionRef.deposit(token: <-nft)
Esta linha deposita o NFT recém-criado na coleção especificada. Ele usa a função de depósito no receiverCollectionRef
para transferir a propriedade do NFT da conta de execução da transação para a coleção. O símbolo <-
aqui também indica que o NFT está sendo movido como um recurso durante o processo deposit
.
Usamos um script para visualizar ou ler dados do blockchain. Os scripts são gratuitos e não precisam de assinatura.
Siga as etapas abaixo para criar um script na pasta do seu projeto.
Primeiro, vá para a pasta do projeto e abra a pasta cadence
. Dentro dele, abra a pasta script
e crie um novo arquivo com o nome view_nft.cdc
.
import NonFungibleToken from "../contracts/NonFungibleToken.cdc" import Collectibles from "../contracts/Collectibles.cdc" pub fun main(user: Address, id: UInt64): &NonFungibleToken.NFT? { let collectionCap= getAccount(user).capabilities .get<&{Collectibles.CollectionPublic}>(/public/NFTCollection) ?? panic("This public capability does not exist.") let collectionRef = collectionCap.borrow()! return collectionRef.borrowNFT(id: id) }
Vamos dividir esse código linha por linha:
Primeiro, importamos o contrato NonFungibleToken
e Collectibles
.
pub fun main(acctAddress: Address, id: UInt64): &NonFungibleToken.NFT?
Esta linha define o ponto de entrada do script, que é uma função pública chamada main. A função leva dois parâmetros:
acctAddress
: um parâmetro do tipo Address
que representa o endereço de uma conta no blockchain Flow.
id
: um parâmetro do tipo UInt64
que representa o identificador exclusivo do NFT na coleção.
Em seguida, usamos getCapability
para buscar o recurso Collectibles.Collection
para o acctAddress
especificado. Uma capacidade é uma referência a um recurso que permite acesso às suas funções e dados. Nesse caso, ele está buscando a capacidade para o tipo de recurso Collectibles.Collection
.
Em seguida, pegamos emprestado um NFT de collectionRef
usando a função borrowNFT
. A função borrowNFT
usa o parâmetro id
, que é o identificador exclusivo do NFT dentro da coleção. A função borrow
em um recurso permite a leitura dos dados do recurso.
Finalmente, retornamos o NFT da função.
Agora é hora de implantar nosso contrato inteligente na testnet Flow.
1. Configure uma conta do Flow.
Execute o seguinte comando no terminal para gerar uma conta Flow:
flow keys generate
Certifique-se de anotar sua chave pública e sua chave privada.
A seguir, iremos para
Cole sua chave pública no campo de entrada especificado.
Mantenha os algoritmos de assinatura e hash definidos como padrão.
Complete o Captcha.
Clique em Criar conta.
Depois de configurar uma conta, recebemos um diálogo com nosso novo endereço de Flow contendo 1.000 tokens de teste de Flow. Copie o endereço para que possamos usá-lo daqui para frente .
2. Configure o projeto.
Agora vamos configurar nosso projeto. Inicialmente, quando montamos o projeto, ele criou um arquivo flow.json
.
Este é o arquivo de configuração do Flow CLI e define a configuração das ações que o Flow CLI pode executar para você. Pense nisso como aproximadamente equivalente a hardhat.config.js
no Ethereum.
Agora, abra seu editor de código e copie e cole o código abaixo em seu arquivo flow.json
.
{ "contracts": { "Collectibles": "./cadence/contracts/Collectibles.cdc", "NonFungibleToken": { "source": "./cadence/contracts/NonFungibleToken.cdc", "aliases": { "testnet": "0x631e88ae7f1d7c20" } } }, "networks": { "testnet": "access.devnet.nodes.onflow.org:9000" }, "accounts": { "testnet-account": { "address": "ENTER YOUR ADDRESS FROM FAUCET HERE", "key": "ENTER YOUR GENERATED PRIVATE KEY HERE" } }, "deployments": { "testnet": { "testnet-account": [ "Collectibles" ] } } }
Cole sua chave privada gerada no local (chave: “INSIRA SUA CHAVE PRIVADA GERADA AQUI”) no código.
Agora, execute o código na testnet. Vá para o terminal e execute o seguinte código:
flow project deploy --network testnet
5. Aguarde a confirmação.
Após enviar a transação, você receberá um ID da transação. Aguarde a confirmação da transação na testnet, indicando que o contrato inteligente foi implantado com sucesso.
Verifique seu contrato implantado aqui .
Verifique o código completo no GitHub .
Parabéns! Agora você construiu um portal de colecionáveis no blockchain Flow e o implantou na testnet. Qual é o próximo? Agora, você pode trabalhar na construção do frontend que abordaremos na parte 2 desta série.
Tenha um ótimo dia!
Também publicado aqui