Blockchain Blog Sol

Crie seu token colecionável na rede RSK

Como criar token colecionável na rede RSK, utilizando Truffle framework e os smart contracts da Open Zeppelin. Além disto, vamos construir um frontend em React para interagir com o token ERC721 publicado na RSK testnet através do Metamask.

Title

Neste tutorial você vai aprender programação Blockchain a partir do zero, construindo uma aplicação descentralizada (decentralized application - dApp) completa, passo por passo. Você aprenderá como criar token colecionável no Blockchain RSK usando Truffle framework, os smart contracts da Open Zeppelin e construir um frontend em React, utilizando create-react-app.

Nós criaremos uma dApp inspirada no Cryptokitties, um pupular jogo Blockchain onde você pode colecionar e criar gatos digitais. Neste tutorial, ao invés de colecionar felinos, em nosso app nos colecionaremos tokens de cores exclusivas.

Fungível x infungível

Um token fungível representa um ativo que pode ser trocado por qualquer outro de igual valor na sua classe.

O dinheiro é um exemplo de token fungível. Uma nota de R$100,00 é igual a qualquer outra nota de R$100,00, você pode trocá-las livremente porque elas tem o mesmo valor, não importando o número de série de uma nota específica de R$100,00. Elas são notas fungíveis.

Por outro lado, um token infungível (Non-Fungible Token - NFT) é um token único, portanto itens colecionáveis são ativos infungíveis e podem ser representados por NFTs.

ERC-721

ERC-721 foi o primeiro padrão para representar tokens infungíveis, sendo que até hoje é o mais popular.

As mais importantes características deste tipo de ativo é ter uma um jeito de verificar quem é o dono e uma forma de transferir os ativos.

É fácil criar um smart contract no padrão ERC-721 importando contratos da biblioteca da OpenZeppelin e nós faremos iso neste tutorial.

A interface para o padrão ERC-721 tem dois métodos:

  • ownerOf: para saber quem é o dono do token
  • transferFrom: para transferir a propriedade de um token

E isto é o suficiente para representar um NFT!

Colors

Neste tutorial, nós criaremos um token infungível para representar nossas cores colecionáveis.

Você poderá criar novos tokens de cores e reivindicá-los para que possam ser guardados em uma carteira blockchain.

Overview

Aqui está um resumo das etapas que faremos para construir nosso token:

  1. Pré requisitos
  2. Inicializar projeto com Truffle e Open Zeppelin;
  3. Configurar Truffle para se conectar na testnet RSK;
  4. Criar uma carteira e adquirir alguns testnet R-BTCs;
  5. Inicializar o frontend;
  6. Adicionar mais algumas configurações ao Truffle;
  7. Criar o smart contract do token e compilar;
  8. Preparar o deploy no Truffle;
  9. Fazer a publicação na testnet RSK com Truffle;
  10. Customizar o frontent;
  11. Interagir com o smart contract.

Video

Caso prefira assistir um video, eu ministrei um workshop online sobre este tutorial: clique aqui

Passos 1 a 4

Os passos 1 a 4 são explicados detalhadamente no tutorial:

Pré-requisitos

  1. Node.js e NPM (Node Package Manager)
  2. Visual Studio Code (VSCode) ou outro editor à sua escolha
  3. Truffle
  4. Metamask - extensão para o google chrome

Os pré-requisitos 1 a 3 também são explicados detalhadamente no tutorial:

O pré-requisito 4, instalar Metamask, conectar a testnet RSK e ganhar alguns tR-BTCs, está explicado passo a passo no tutorial:

Setup the project

Crie uma nova pasta chamada colors:

mkdir colors
cd colors

Na pasta colors, execute as etapas abaixo, seguindo as instruções do tutorial já citado anteriormente: Setup a project with Truffle and OpenZeppelin

  1. Inicialize um novo projeto Truffle;
  2. Inicialize um projeto npm;
  3. Instale Open Zeppelin;
  4. Instale HD wallet provider;
  5. Crie um mnemonic para uma carteira;
  6. Crie o arquivo .secret;
  7. Verifique o gas price na testnet;
  8. Configure Truffle para se conectar à testnet RSK;
  9. Use o console Truffle;
  10. Teste a conexão a testnet RSK;
  11. Saiba os endereços da sua wallet;
  12. Consulte o saldo;
  13. Ganhe alguns testnet R-BTCs no faucet;

Initialize a aplicação client

Precisamos de 3 itens para construir o frontend:

  1. Create React App
  2. Web3.js
  3. Bootstrap

Create React App

Este é o template oficial para criar uma aplicação React de página única (single-page React application). Com ele faremos uma instalação sem precisar configurar mais nada.

Para saber mais: create react app

No diretório do projeto, no terminal, execute:

npx create-react-app app --use-npm

create-react-app

A opção --use-npm é para escolher o npm como gerenciador de pacotes.

npx está incluído com o npm versão 5.2+ ou superior, se precisar, veja instruções para versões antigas de npm.

Este é um grande pacote e demora um pouco para apresentar a mensagem de instalação realizada com sucesso:

create-react-app successful installation

Em nosso projeto, agora existe uma nova pasta chamada app. Nós customizaremos o frontend mais tarde, neste diretório.

create-react-app file structure

Web3.js

Web3.js auxilia o desenvolvimento de websites ou clients que interagem com o blockchain, com instruções para ler e escrever dados no Blockchain através dos smart contracts.

A biblioteca web3.js é uma API Javascript Ethereum que faz as conexões utilizando a especificação JSON-RPC. Como a máquina virtual da RSK é compatível com a Ethereum Virtual Machine (EVM), é possível utilizar web3.js para realizar a comunicação entre o front-end e um nó.

Para instalar web3.js, na pasta app, digite o comando abaixo no terminal e pressione enter:

cd app
npm install -E web3@1.2.7

A opção -E é para salvar as dependências na versão especificada ao invés da versão padrão definida no npm.

web3 install

web3 successful installation

Mais informações:

web3.js

Bootstrap

Também na pasta app, digite o comando abaixo no terminal e pressione enter:

cd app
npm install -E bootstrap@4.4.1

web3 install

Como já dito anteriormente, a opção -E é para salvar as dependências na versão especificada ao invés da versão padrão definida no npm.

web3 successful installation

Configurando Truffle

Volte para o diretório do projeto, colors, abra o arquivo truffle-config.js no VS Code e sobrescreva como o seguinte código:

const HDWalletProvider = require('@truffle/hdwallet-provider');

const fs = require('fs');
const mnemonic = fs.readFileSync(".secret").toString().trim();
if (!mnemonic || mnemonic.split(' ').length !== 12) {
  throw new Error('unable to retrieve mnemonic from .secret');
}

const gasPriceTestnetRaw = fs.readFileSync(".gas-price-testnet.json").toString().trim();
const gasPriceTestnet = parseInt(JSON.parse(gasPriceTestnetRaw).result, 16);
if (typeof gasPriceTestnet !== 'number' || isNaN(gasPriceTestnet)) {
  throw new Error('unable to retrieve network gas price from .gas-price-testnet.json');
}
console.log("Gas price Testnet: " + gasPriceTestnet);

const path = require("path");

module.exports = {
  networks: { 
    testnet: {
      provider: () => new HDWalletProvider(mnemonic, 'https://public-node.testnet.rsk.co/1.3.0/'),
      network_id: 31,
      gasPrice: Math.floor(gasPriceTestnet * 1.1),
      networkCheckTimeout: 1e9
    },
  }, 

  contracts_build_directory: path.join(__dirname, "app/src/contracts"), 
  
  compilers: {
    solc: {
      version: "0.5.7",
    }
  }
}

Ficará deste jeito:

truffle-config

Sobre contractsbuilddirectory

Nós adicionamos a bibliteca path para utilizá-la com um novo parâmetro chamado contracts_build_directory, que define o local onde as informações dos contratos (contracts artifacts) são armazenadas, como a abi e os endereços de publicação nas diversas redes.

Em nosso projeto, estará localizada em uma pasta diferente: app/src/contracts.

Arquitetura dos smart contracts

Criaremos o smart contract Color.sol que vai herdar as definições do ERC721 que estão na biblioteca OZ.

Crie o smart contract Color.sol

No diretório Contracts, crie um arquivo chamado Color.sol.

Create Color.sol

File Color.sol

Copie e cole o smart contract deste link, ou do código abaixo:

Color.sol

pragma solidity 0.5.7;

import '@openzeppelin/contracts/token/ERC721/ERC721Full.sol';

contract Color is ERC721Full {

  bytes3[] public colors;
  mapping(bytes3 => bool) private _colorExists;

  constructor() ERC721Full("Color", "COLOR") public {
  }

  // E.G. color = "#FFFFFF"
  function mint(bytes3 _color) public {
    require(!_colorExists[_color], "color exists");
    uint _id = colors.push(_color);
    _mint(msg.sender, _id);
    _colorExists[_color] = true;
  }
}

Ficará assim:

Color.sol

Entendendo o smart contract

Para criar nosso token ERC721, importaremos o ERC721Full do Open Zeppelin. Esta biblioteca importa algumas outras, como SafeMath.sol, o padrão para o ERC721 e algumas características extras, como metadata e a listagem de tokens.

Com o metadata podemos customizar nosso token definindo um nome e símbolo para ele no construtor.

A função constructor é executada apenas uma vez, no momento em que o smart contract é publicado no blockchain.

No construtor estamos chamando a função constructor do smart contract ERC721Full e passando como argumentos o nome Color e símbolo COLOR.

O gerenciamento de cores é executado com a variável colors, que é um array de cores, além da váriável _colorExists, que é um índice para saber se uma cor já foi emitida.

Também temos uma função para criar novos tokens de cores. Está função recebe um argumento do tipo bytes3, que é um número hexadecimal que corresponde a cor do token que será criado.

Por exemplo, para criar um token verde, passe como argumento "#00FF00" quando chamar a função. Ou se quiser criar um token vermelho, use "#FF0000".

Compile o smart contract

No terminal, execute este comando:

truffle compile

truffle compile

Publique o smart contract na testnet

Primeiro é necessário criar um arquivo na estrutura do Truffle com instruções para publicar o smart contract.

Crie o arquivo 2deploycontracts.js

O diretório migrations contém arquivos JavaScript que auxiliam a publicação de contratos no blockchain. Estes arquivos são responsáveis pelo preparo das tarefas de implantação e são escritos com base no pressuposto de que suas necessidades de implantação podem mudar com o tempo. I histórico das publicações executadas anteriormente é salvo no blockchain através de um contrato especial criado automaticamente pelo Truffle, chamado Migrations. (source: running migrations)

Na pasta migrations, crie o arquivo 2_deploy_contracts.js

create 2_deploy_contracts

Copie e cole este código:

const Color = artifacts.require("Color");

module.exports = function(deployer) {
  deployer.deploy(Color);
};

2_deploy_contracts

Migrate

No terminal, execute este comando:

truffle migrate --network testnet

Aguarde alguns minutos para que as transações da publicação do smart contract sejam incluídas no blockchain.

O comando migrate pode compilar o smart contract de novo, se for necessário.

truffle migrate compile

Primeiro é publicado o smart contract Migrations.sol, arquivo gerado pelo Truffle:

truffle migrate Migrations.sol

Está é a transação da minha publicação na RSK testnet:

0x3de61b8983dc3db2ca21a9d10106a19c445885fcb7040774bd6937daf94a4702

Em seguida, é publicado nosso smart contract Color.sol:

truffle migrate Color.sol

Aqui está a trasanção na RSK testnet:

0x2c2d2932a7d637fbba100b5c482c1fa1899c4fe24bd1a458976a93cee6c5ba85

Uma dica: se acontecer algum problema de comunicação com a testnet entre a publicação do Migrations.sol e do Color.sol, apenas execute o comando migrate novamente, que ele executa o deploy apenas do que estiver faltando.

Parabéns!

Nosso NFT Color está publicado na RSK Testnet.

Salve o endereço do contrato do token, ele poderá ser útil depois:

tokenAddress = "0x5505a54a8F3e63D37095c37d9f8AcF0f4900B61F"

Aplicação client

Agora vamos construir o frontend que vai interagir com o smart contract. Com ele será possível criar novos tokens de cores e listar todos os tokens que existem na sua carteira.

Na pasta app, vamos customizar alguns arquivos.

index.html

Abra o arquivo index.html na pasta app\public.

Na seção head, atualize o title:

<title>NFT Colors</title>

index.js

Na pasta app\src, abra o arquivo index.js e adicione uma linha para utilizar bootstrap em nosso projeto

import 'bootstrap/dist/css/bootstrap.css';

Também apague está linha:

import './index.css';

O arquivo index.js ficará como este. Se quiser, pode copiar deste link ou do código abaixo:

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

Agora seu index.js ficará desta forma:

index.js

App.css

Abra o arquivo App.css e sobrescreva com o código abaixo:

App.css

.token {
  height: 150px;
  width: 150px;
  border-radius: 50%;
  display: inline-block;
}

Este arquivo customiza a aparência dos tokens.

Este é o resultado:

App.css

App.js

Abra o arquivo App.js e sobrescreva com o código abaixo:

App.js

import React, { Component } from 'react';
import Web3 from 'web3';
import './App.css';
import Color from './contracts/Color.json';

function colorHexToString(hexStr) {
  return '#' + hexStr.substring(2);
}

function colorStringToBytes(str) {
  if (str.length !== 7 || str.charAt(0) !== '#') {
    throw new Error('invalid color string');
  }
  const hexStr = '0x' + str.substring(1);
  return Web3.utils.hexToBytes(hexStr);
}

class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      account: '',
      contract: null,
      totalSupply: 0,
      colors: [],
    };
  }

  async componentWillMount() {
    await this.loadWeb3();
    await this.loadBlockchainData();
  }

  async loadWeb3() {
    if (window.ethereum) {
      // current web3 providers
      window.web3 = new Web3(window.ethereum);
      await window.ethereum.enable();
    }
    else if (window.web3) {
      // fallback for older web3 providers
      window.web3 = new Web3(window.web3.currentProvider);
    }
    else {
      // no web3 provider, user needs to install one in their browser
      window.alert(
        'No injected web3 provider detected');
    }
    console.log(window.web3.currentProvider);
  }

  async loadBlockchainData() {
    const web3 = window.web3;
    // Load account
    const accounts = await web3.eth.getAccounts();
    console.log ('account: ', accounts[0]);
    this.setState({ account: accounts[0] });
    const networkId = await web3.eth.net.getId();
    const networkData = Color.networks[networkId];

    if (!networkData) {
      window.alert('Smart contract not deployed to detected network.');
      return;
    }

    const abi = Color.abi;
    const address = networkData.address;
    const contract = new web3.eth.Contract(abi, address);
    this.setState({ contract });
    const totalSupply = await contract
      .methods.totalSupply().call();
    this.setState({ totalSupply });

    // Load Colors
    for (var i = 1; i <= totalSupply; i++) {
      const colorBytes = await contract
        .methods.colors(i - 1).call();
      const colorStr = colorHexToString(colorBytes);
      this.setState({
        colors: [...this.state.colors, colorStr],
      });
    }
  }

  mint = (colorStr) => {
    const colorBytes = colorStringToBytes(colorStr);
    this.state.contract.methods
      .mint(colorBytes)
      .send({ from: this.state.account })
      .once('receipt', (receipt) => {
        console.log ('transaction receipt: ', receipt)
        this.setState({
          colors: [...this.state.colors, colorStr],
        });
      });
  }

  render() {
    return (
      <div>
        <nav className="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow">
          <span className="navbar-brand col-sm-3 col-md-2 mr-0">
            Color Tokens
          </span>
          <ul className="navbar-nav px-3">
            <li className="nav-item text-nowrap d-none d-sm-none d-sm-block">
              <small className="text-white"><span id="account">{this.state.account}</span></small>
            </li>
          </ul>
        </nav>
        <div className="container-fluid mt-5">
          <div className="row">
            <main role="main" className="col-lg-12 d-flex text-center">
              <div className="content mr-auto ml-auto">
                <h1>Issue Token</h1>
                <form onSubmit={(event) => {
                  event.preventDefault();
                  const colorStr = this.color.value;
                  this.mint(colorStr);
                }}>
                  <input
                    type='text'
                    className='form-control mb-1'
                    placeholder='e.g. #FF00FF'
                    ref={(input) => { this.color = input }}
                  />
                  <input
                    type='submit'
                    className='btn btn-block btn-primary'
                    value='MINT'
                  />
                </form>
              </div>
            </main>
          </div>
          <hr/>
          <div className="row text-center">
            { this.state.colors.map((colorStr, key) => {
              return (
                <div key={key} className="col-md-3 mb-3">
                  <div className="token" style={{ backgroundColor: colorStr }}></div>
                  <div>{colorStr}</div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    );
  }
}

export default App;

Entendendo App.js

Depois de importar web3 aqui:

import Web3 from 'web3'

Esta parte faz a conexão com a RSK testnet utilizando a carteira injetada (web3 injected), no nosso caso, Metamask:

  async loadWeb3() {
    if (window.ethereum) {
      // current web3 providers
      window.web3 = new Web3(window.ethereum);
      await window.ethereum.enable();
    }
    else if (window.web3) {
      // fallback for older web3 providers
      window.web3 = new Web3(window.web3.currentProvider);
    }
    else {
      // no web3 provider, user needs to install one in their browser
      window.alert(
        'No injected web3 provider detected');
    }
    console.log(window.web3.currentProvider);
  }

Para carregar a instância do smart contract Color que já está publicado, precisamos carregar algumas informações do arquivo de artifatos do Truffle:

import Color from './contracts/Color.json';

E depois da conexão com sucesso com o web3, a função loadBlockchainData carrega as contas, informações da rede e o smart contract Color.

  async loadBlockchainData() {
    const web3 = window.web3;
    // Load account
    const accounts = await web3.eth.getAccounts();
    console.log ('account: ', accounts[0]);
    this.setState({ account: accounts[0] });
    const networkId = await web3.eth.net.getId();
    const networkData = Color.networks[networkId];

    if (!networkData) {
      window.alert('Smart contract not deployed to detected network.');
      return;
    }

    const abi = Color.abi;
    const address = networkData.address;
    const contract = new web3.eth.Contract(abi, address);
    this.setState({ contract });
    const totalSupply = await contract
      .methods.totalSupply().call();
    this.setState({ totalSupply });

    // Load Colors
    for (var i = 1; i <= totalSupply; i++) {
      const colorBytes = await contract
        .methods.colors(i - 1).call();
      const colorStr = colorHexToString(colorBytes);
      this.setState({
        colors: [...this.state.colors, colorStr],
      });
    }
  }

Também temos a função mint que envia uma transação para o blockchain chamando o método mint no smart contract.

  mint = (colorStr) => {
    const colorBytes = colorStringToBytes(colorStr);
    this.state.contract.methods
      .mint(colorBytes)
      .send({ from: this.state.account })
      .once('receipt', (receipt) => {
        console.log ('transaction receipt: ', receipt)
        this.setState({
          colors: [...this.state.colors, colorStr],
        });
      });
  }

E, finalmente, a função render() é responsável pelo código HTML da aplicação. São basicamente 3 partes:

  • Apresentar o endereço ativo (selecionado no Metamask) na barra de navegação
  • Um formulário para a emissão de novos tokens
  • Listar todos os tokens emitidos para a conta ativa, em forma de tabela, mostrando a cor e o código de cada token.

Running

No diretório app, no terminal, executar:

npm start

npm start

Será aberto o browser default no endereço http://localhost:3000/

Se não abrir, você pode manualmente copiar e colar a url no browser.

localhost:3000

Metamask automaticamente detectará que nossa aplicação quer se conectar com a carteira, então temos que autorizar clicando no botão Connect.

authorize Metamask

E aqui está nosso frontend!

frontend

Interagindo com o smart contract

As cores são salvas utilizando a representação hexadecimal de cada uma.

Para saber mais sobre cores e seus códigos hexa:

Alguns códigos de cores:

Red #FF0000
Green #00FF00
Blue #0000FF
Yellow #FFFF00

Mint - emitindo tokens

Escolha uma cor, preencha seu valor hexadecimal no campo de texto e clique no botão MINT.

Será chamada a função mint() no smart contract Color, com o parâmetro da cor que você definiu.

Vamos criar a cor vermelha, #FF0000.

Não esqueça do símbolo #

#FF0000

#FF0000 Metamask confirm

Clique no botão confirm.

Aguarde alguns segundos até que a transação seja incluída em um bloco…

Ótimo! Você já tem seu primeiro token colecionável de cores:

Red minted

Vamos criar a cor azul: #0000FF

#0000FF

Aguarde alguns segundos até que a transação seja minerada…

E agora temos duas cores na coleção!

Blue minted

A coleção está crescendo!

color collection

Parabéns!

Você imaginava que era tão fácil criar um NFT?

Eu mostrei como conectar o framework Truffle com a rede RSK e publicar seu próprio token colecionável utilizando a biblioteca Open Zeppelin.

Este tutorial foi inspirado em outro criado por Gregory McCubbin, da dApp University. Confira o original.

Nosso objetivo é unir forças e dar opções para as pessoas que acreditam nos smart contracts baseados em Ethereum e também na força do Bitcoin, fazer isso na rede RSK.

Espero que esse tutorial tenha sido útil e agradeço caso tenha algum feedback para mim. Compartilhe o artigo caso tenha gostado :)

Se quiser ver outros videos, assine meu canal: youtube Solange Gueiros

© 2020, Solange Gueiros