En este tutorial, aprenderemos cómo crear un sitio web para recolectar objetos coleccionables digitales (o NFT) en blockchain Flow. Usaremos el lenguaje de contrato inteligente Cadence junto con React para que todo esto suceda. También aprenderemos sobre Flow, sus ventajas y las divertidas herramientas que podemos utilizar.
Al final de este artículo, tendrá las herramientas y el conocimiento que necesita para crear su propia aplicación descentralizada en la cadena de bloques Flow.
¡Vamos a sumergirnos de lleno!
Estamos creando una aplicación para coleccionables digitales. Cada objeto de colección es un token no fungible (NFT).
Para que todo esto funcione, usaremos el estándar NonFungibleToken de Flow, que es un conjunto de reglas que nos ayuda a administrar estos elementos digitales especiales (similar a ERC-721 en Ethereum).
Antes de comenzar, asegúrese de instalar Flow CLI en su sistema. Si no lo has hecho, sigue estas instrucciones de instalación .
Si está listo para poner en marcha su proyecto, primero escriba la configuración del flujo de comandos.
Este comando hace algo de magia detrás de escena para establecer las bases de su proyecto. Crea un sistema de carpetas y configura un archivo llamado flow.json para configurar su proyecto, ¡asegurándose de que todo esté organizado y listo para funcionar!
El proyecto contendrá una carpeta cadence
y un archivo flow.json
. (Un archivo flow.json es un archivo de configuración para su proyecto, que se mantiene automáticamente).
La carpeta Cadencia contiene lo siguiente:
Siga los pasos a continuación para utilizar Flow NFT Standard.
Primero, vaya a la carpeta flow-collectibles-portal
y busque la carpeta cadence
. Luego, abra la carpeta contracts
. Cree un archivo nuevo y asígnele el nombre NonFungibleToken.cdc
.
Ahora, abra el enlace llamado NonFungibleToken que contiene el estándar NFT. Copie todo el contenido de ese archivo y péguelo en el nuevo archivo que acaba de crear ("NonFungibleToken.cdc").
¡Eso es todo! Ha configurado con éxito los estándares para su proyecto.
Ahora, ¡escribamos algo de código!
Sin embargo, antes de sumergirnos en la codificación, es importante que los desarrolladores establezcan un modelo mental de cómo estructurar su código.
En el nivel superior, nuestro código base consta de tres componentes principales:
NFT: Cada coleccionable se representa como un NFT.
Colección: una colección se refiere a un grupo de NFT propiedad de un usuario específico.
Funciones y variables globales: son funciones y variables definidas a nivel global para el contrato inteligente y no están asociadas con ningún recurso en particular.
Cree un nuevo archivo llamado Collectibles.cdc
dentro de cadence/contracts
. Aquí es donde escribiremos el código.
Estructura del contrato
import NonFungibleToken from "./NonFungibleToken.cdc" pub contract Collectibles: NonFungibleToken{ pub var totalSupply: UInt64 // other code will come here init(){ self.totalSupply = 0 } }
Analicemos el código línea por línea:
Primero, necesitaremos estandarizar que estamos creando una NFT incluyendo el llamado "NonFungibleToken". Este es un estándar NFT creado por Flow que define el siguiente conjunto de funcionalidades que debe incluir cada contrato inteligente NFT.
Después de importar, creemos nuestro contrato. Para hacer eso, usamos pub contract [contract name]
. Utilice la misma sintaxis cada vez que cree un nuevo contrato. Puede completar el contract name
con el nombre que desee llamar a su contrato. En nuestro caso, llamémoslo Collectibles
.
A continuación, queremos asegurarnos de que nuestro contrato siga un determinado conjunto de funcionalidades y reglas de NonFungibleToken. Para hacer eso, agregamos una interfaz NonFungibleToken con la ayuda de `:`.
Así ( `pub contract Collectibles: NonFungibleToken{}`
)
Cada contrato DEBE tener la función init()
. Se llama cuando el contrato se implementa inicialmente. Esto es similar a lo que Solidity llama Constructor.
Ahora, creemos una variable global llamada totalSupply
con un tipo de datos UInt64
. Esta variable realizará un seguimiento del total de tus coleccionables.
Ahora, inicialice totalSupply
con el valor 0
.
¡Eso es todo! Establecimos las bases de nuestro contrato Collectibles
. Ahora podemos comenzar a agregar más características y funcionalidades para hacerlo aún más interesante.
Antes de continuar, consulte el fragmento de código para comprender cómo definimos las variables en Cadence:
Agregue el siguiente código a su 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 ha visto antes, el contrato implementa la interfaz estándar NFT, representada por pub contract Collectibles: NonFungibleToken
. De manera similar, los recursos también pueden implementar varias interfaces de recursos.
Entonces, agreguemos la interfaz NonFungibleToken.INFT
al recurso NFT, que exige la existencia de una propiedad pública llamada id dentro del recurso.
Estas son las variables que usaremos en el recurso NFT:
id:
Mantiene el ID de NFTname:
Nombre del NFT.image:
URL de imagen de NFT.
Después de definir la variable, asegúrese de inicializarla en la función init()
.
Sigamos adelante y creemos otro recurso llamado Collection Resource
.
Primero, es necesario comprender cómo funcionan Collection Resources
.
Si necesitas almacenar un archivo de música y varias fotos en tu portátil, ¿qué harías?
Normalmente, navegaría a una unidad local (digamos su D-Drive) y crearía una carpeta music
y una carpeta photos
. Luego copiarías y pegarías los archivos de música y fotos en tus carpetas de destino.
De manera similar, así es como funcionan tus coleccionables digitales en Flow.
Imagine su computadora portátil como una Flow Blockchain Account
, su D-Drive como Account Storage
y su carpeta como una Collection
.
Entonces, al interactuar con cualquier proyecto para comprar NFT, el proyecto crea su collection
en account storage
, similar a crear una carpeta en su D-Drive. Cuando interactúas con 10 proyectos NFT diferentes, terminarás con 10 colecciones diferentes en tu cuenta.
¡Es como tener un espacio personal para almacenar y organizar tus tesoros digitales únicos!
import NonFungibleToken from "./NonFungibleToken.cdc" pub contract Collectibles: NonFungibleToken{ //Above code NFT Resource… // Collection Resource pub resource Collection{ } // Below code… }
Cada collection
tiene una variable ownedNFTs
para contener los NFT Resources
.
pub resource Collection { pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} init(){ self.ownedNFTs <- {} } }
Interfaces de recursos
Una interfaz resource
en Flow es similar a las interfaces de otros lenguajes de programación. Se ubica encima de un recurso y garantiza que el recurso que lo implementa tenga la funcionalidad requerida según lo definido por la interfaz.
También se puede utilizar para restringir el acceso a todo el recurso y ser más restrictivo en términos de modificadores de acceso que el recurso en sí.
En el estándar NonFungibleToken
, existen varias interfaces de recursos como INFT
, Provider
, Receiver
y CollectionPublic
.
Cada una de estas interfaces tiene funciones y campos específicos que deben ser implementados por el recurso que las utiliza.
En este contrato, usaremos estas tres interfaces de NonFungibleToken: Provider
, Receiver
y CollectionPublic
. Estas interfaces definen funciones como deposit
, withdraw
, borrowNFT
y getIDs
. Explicaremos cada uno de estos en detalle a medida que avancemos.
También agregaremos algunos eventos que emitiremos desde estas funciones, así como también declararemos algunas variables que usaremos más adelante en el 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
Ahora, creemos la función withdraw()
requerida por la interfaz.
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()... }
Con la ayuda de esta función, puede sacar el recurso NFT de la colección. Si se:
Luego, la persona que llama puede utilizar este recurso y guardarlo en el almacenamiento de su cuenta.
Depósito
Ahora es el momento de la función deposit()
requerida 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()... }
Pedir prestado y obtener ID
Ahora, centrémonos en las dos funciones requeridas por NonFungibleToken.CollectionPublic: borrowNFT()
y 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()... }
Incinerador de basuras
Lo último que necesitamos para el recurso de colección es un destructor.
destroy (){ destroy self.ownedNFTs }
Dado que el recurso Colección contiene otros recursos (recursos NFT), debemos especificar un destructor. Un destructor se ejecuta cuando se destruye el objeto. Esto garantiza que los recursos no queden "sin hogar" cuando se destruye su recurso principal. No necesitamos un destructor para el recurso NFT ya que no contiene ningún otro recurso.
Veamos el código fuente completo de los recursos de la colección:
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() } }
Ahora hemos terminado todos los recursos. A continuación, veremos la función global.
Las funciones globales son funciones que se definen en el nivel global del contrato inteligente, lo que significa que no forman parte de ningún recurso. Estos son accesibles y convocados por el público, y exponen la funcionalidad principal del contrato inteligente al público.
createEmptyCollection : esta función inicializa una Collectibles.Collection
vacía en el almacenamiento de la cuenta de la persona que llama.
checkCollection : esta práctica función le ayuda a descubrir si su cuenta ya tiene un recurso collection
.
mintNFT : esta función es genial porque permite a cualquiera crear un 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()...
Y ahora, FINALMENTE, con todo en su lugar, hemos terminado de redactar nuestro contrato inteligente. Eche un vistazo al código final aquí .
Ahora, veamos cómo interactúa un usuario con los contratos inteligentes implementados en la cadena de bloques Flow.
Hay dos pasos para interactuar con la cadena de bloques Flow:
Las transacciones son datos firmados criptográficamente que contienen un conjunto de instrucciones que interactúan con el contrato inteligente para actualizar el estado del flujo. En términos simples, esto es como una llamada a una función que cambia los datos en la cadena de bloques. Las transacciones suelen implicar algún coste, que puede variar dependiendo de la blockchain en la que te encuentres.
Una transacción incluye múltiples fases opcionales: prepare
, pre
, execute
y post
.
Puede leer más sobre esto en el documento de referencia de Cadence sobre transacciones . Cada fase tiene un propósito; Las dos fases más importantes son prepare
y execute
.
Prepare Phase
: esta fase se utiliza para acceder a datos e información dentro de la cuenta del firmante (permitido por el tipo AuthAccount).
Execute Phase
: esta fase se utiliza para ejecutar acciones.
Ahora, creemos una transacción para nuestro proyecto.
Siga los pasos a continuación para crear una transacción en la carpeta de su proyecto.
Primero, vaya a la carpeta del proyecto y abra la carpeta cadence
. Dentro de él, abra la carpeta transaction
y cree un nuevo archivo con el nombre Create_Collection.cdc
y 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) } } }
Analicemos este código línea por línea:
Esta transacción interactúa con el contrato inteligente de Coleccionables. Luego, verifica si el remitente (firmante) tiene un recurso de Colección almacenado en su cuenta tomando prestada una referencia al recurso de Colección de la ruta de almacenamiento especificada Collectibles.CollectionStoragePath
. Si la referencia es nula, significa que el firmante aún no tiene una colección.
Si el firmante no tiene una colección, crea una colección vacía llamando a la función createEmptyCollection()
.
Después de crear la colección vacía, colóquela en la cuenta del firmante en la ruta de almacenamiento especificada Collectibles.CollectionStoragePath
.
Esto establece un vínculo entre la cuenta del firmante y la colección recién creada 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) } }
Analicemos este código línea por línea:
Primero importamos el Collectibles contract
NonFungibleToken
y Collectibles.
transaction(name: String, image: String)
Esta línea define una nueva transacción. Se necesitan dos argumentos, nombre e imagen, ambos de tipo Cadena. Estos argumentos se utilizan para pasar el nombre y la imagen del NFT que se está acuñando.
let receiverCollectionRef: &{NonFungibleToken.CollectionPublic}
Esta línea declara una nueva variable receiverCollectionRef.
Es una referencia a una colección pública de NFT de tipo NonFungibleToken.CollectionPublic
. Esta referencia se utilizará para interactuar con la colección donde depositaremos el NFT recién acuñado.
prepare(signer: AuthAccount)
Esta línea inicia el bloque de preparación, que se ejecuta antes de la transacción. Se necesita un argumento firmante de tipo AuthAccount
. AuthAccount
representa la cuenta del firmante de la transacción.
Toma prestada una referencia a la Collectibles.Collection
del almacenamiento del firmante dentro del bloque de preparación. Utiliza la función de préstamo para acceder a la referencia de la colección y almacenarla en la variable receiverCollectionRef
.
Si no se encuentra la referencia (si la colección no existe en el almacenamiento del firmante, por ejemplo), arrojará el mensaje de error "no se pudo tomar prestada la referencia de la colección".
El bloque execute
contiene la lógica de ejecución principal de la transacción. El código dentro de este bloque se ejecutará después de que el bloque prepare
se haya completado con éxito.
nft <- Collectibles.mintNFT(_name: name, image: image)
Dentro del bloque execute
, esta línea llama a la función mintNFT
desde el contrato Collectibles
con los argumentos de nombre e imagen proporcionados. Se espera que esta función cree un nuevo NFT con el nombre y la imagen proporcionados. El símbolo <-
indica que el NFT se recibe como un objeto que se puede mover (un recurso).
self.receiverCollectionRef.deposit(token: <-nft)
Esta línea deposita el NFT recién creado en la colección especificada. Utiliza la función de depósito en receiverCollectionRef
para transferir la propiedad del NFT desde la cuenta de ejecución de la transacción a la colección. El símbolo <-
aquí también indica que el NFT se mueve como recurso durante el proceso deposit
.
Usamos un script para ver o leer datos de la cadena de bloques. Los scripts son gratuitos y no necesitan firma.
Siga los pasos a continuación para crear un script en la carpeta de su proyecto.
Primero, vaya a la carpeta del proyecto y abra la carpeta cadence
. Dentro de él, abra la carpeta script
y cree un nuevo archivo con el nombre 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) }
Analicemos este código línea por línea:
Primero, importamos el contrato NonFungibleToken
y Collectibles
.
pub fun main(acctAddress: Address, id: UInt64): &NonFungibleToken.NFT?
Esta línea define el punto de entrada del script, que es una función pública denominada main. La función toma dos parámetros:
acctAddress
: un parámetro de tipo Address
que representa la dirección de una cuenta en la cadena de bloques Flow.
id
: un parámetro de tipo UInt64
que representa el identificador único del NFT dentro de la colección.
Luego usamos getCapability
para recuperar la capacidad Collectibles.Collection
para la acctAddress
especificada. Una capacidad es una referencia a un recurso que permite el acceso a sus funciones y datos. En este caso, se está obteniendo la capacidad para el tipo de recurso Collectibles.Collection
.
Luego, tomamos prestado un NFT de collectionRef
usando la función borrowNFT
. La función borrowNFT
toma el parámetro id
, que es el identificador único del NFT dentro de la colección. La función borrow
en una capacidad permite leer los datos del recurso.
Finalmente, devolvemos el NFT de la función.
Ahora es el momento de implementar nuestro contrato inteligente en la red de prueba Flow.
1. Configura una cuenta Flow.
Ejecute el siguiente comando en la terminal para generar una cuenta Flow:
flow keys generate
Asegúrese de anotar su clave pública y su clave privada.
A continuación, nos dirigiremos a
Pegue su clave pública en el campo de entrada especificado.
Mantenga los algoritmos de firma y hash configurados de forma predeterminada.
Completa el Captcha.
Haga clic en Crear cuenta.
Después de configurar una cuenta, recibimos un diálogo con nuestra nueva dirección de Flow que contiene 1000 tokens de Flow de prueba. Copie la dirección para que podamos usarla en el futuro .
2. Configure el proyecto.
Ahora, configuremos nuestro proyecto. Inicialmente, cuando configuramos el proyecto, creó un archivo flow.json
.
Este es el archivo de configuración para Flow CLI y define la configuración de las acciones que Flow CLI puede realizar por usted. Piense en esto como aproximadamente equivalente a hardhat.config.js
en Ethereum.
Ahora, abra su editor de código y copie y pegue el siguiente código en su archivo 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" ] } } }
Pegue su clave privada generada en el lugar (clave: “INGRESE SU CLAVE PRIVADA GENERADA AQUÍ”) en el código.
Ahora, ejecute el código en la red de prueba. Vaya a la terminal y ejecute el siguiente código:
flow project deploy --network testnet
5. Espere la confirmación.
Después de enviar la transacción, recibirá un ID de transacción. Espere a que se confirme la transacción en la red de prueba, lo que indica que el contrato inteligente se ha implementado correctamente.
Consulta tu contrato desplegado aquí .
Consulta el código completo en GitHub .
¡Felicidades! Ahora ha creado un portal de coleccionables en la cadena de bloques Flow y lo ha implementado en la red de prueba. ¿Que sigue? Ahora puedes trabajar en la construcción de la interfaz que cubriremos en la parte 2 de esta serie.
¡Que tengas un gran día!
También publicado aquí