WEB3DEV

pamelaaline
pamelaaline

Posted on

Armazenamento DApp usando Solidity e IPFS

Imagine uma aplicação de armazenamento descentralizado onde compartilhamos nossos discos uns com os outros sem nenhum servidor no meio. Imagine agora, que cada arquivo nesta aplicação descentralizada possa ser facilmente compartilhado e encontrado com um simples hash. Parece promissor, certo? bem, esse é um projeto em andamento neste exato momento e eu vou explicar como ele funciona.

Esse artigo é uma tradução de Marcos Carlomagno feita por Pamela Aline. Você pode encontrar o artigo original aqui.
Abr 14

Image description
Foto por Shubham Dhage em Unsplash

Introdução

TL;DR: Você pode acessar o código do repositório Github (e deixar uma estrela 😉).

Tudo começou com um único problema que surgiu quando eu estava tentando descobrir como armazenar arquivos grandes em um dApp sem perder a definição do dApp. Se o aplicativo armazena arquivos em um serviço de armazenamento centralizado, então o aplicativo não é mais descentralizado por definição.

Então, após uma pequena pesquisa, encontrei uma solução para resolver este problema: IPFS.

IPFS em um parágrafo

O IPFS é um protocolo de hipermídia ponto a ponto, o que significa um protocolo para compartilhar mídias entre computadores. Usando este protocolo, você pode facilmente construir redes p2p (ponto a ponto) compartilhando informações entre nodes(onde um node pode ser o seu próprio computador).

Felizmente, existe um pacote Javascript de alto nível para acessar e interagir com este protocolo, o que significa que você pode construir um aplicativo Frontend que usa seu computador como um node para compartilhar e acessar arquivos através da rede. Acho que você já tem uma pista de como este aplicativo vai funcionar 😉.

Mas então o problema é como organizar arquivos, usuários, referenciar os metadados dos arquivos de forma descentralizada e criar informações universalmente acessíveis. Aqui é onde a blockchain e os smart contracts vêm para nos ajudar.

Smart Contract

Um smart contract é um programa simples, identificado com um endereço que executa transações em uma rede blockchain. Não vou me aprofundar muito neste conceito, mas, em resumo, podemos usar um contrato inteligente como um pequeno banco de dados devido à sua natureza imutável .

Construindo a aplicação

Criando o Smart Contract com Solidity

A primeira parte consiste em criar um smart contract em linguagem de Solidity para armazenar informações dos arquivos carregados no aplicativo. Vamos salvar informações gerais sobre eles, tais como nome do arquivo, tipo, tamanho, etc.

Como você pode ver, o código é bastante conciso e direto, temos apenas três variáveis, uma função, um evento e uma estrutura que representa um arquivo no sistema. O código é autoexplicativo, mas eu acrescentei alguns comentários para melhor compreensão.

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

contract Persssist {
    string public name = "Persssist";
    uint256 public fileCount = 0;

// este mapeamento se comporta como um "catálogo”
    // de arquivos carregados para o armazenamento, declaramos
    // como público, a fim de acessá-lo diretamente do Frontend
    mapping(uint256 => File) public files;

    struct File {
        uint256 fileId;
        string filePath;
        uint256 fileSize;
        string fileType;
        string fileName;
        address payable uploader;
    }

    event FileUploaded(
        uint256 fileId,
        string filePath,
        uint256 fileSize,
        string fileType,
        string fileName,
        address payable uploader
    );

    // nós carregamos os metadados do arquivo
    // para os arquivos do smart contract
    // mapeando a fim de persistir
    // as informações.
    function uploadFile(
        string memory _filePath,
        uint256 _fileSize,
        string memory _fileType,
        string memory _fileName
    ) public {
        require(bytes(_filePath).length > 0);
        require(bytes(_fileType).length > 0);
        require(bytes(_fileName).length > 0);
        require(msg.sender != address(0));
        require(_fileSize > 0);

        // como os mapeamentos do solidity
        // não têm um atributo de comprimento
        // a maneira mais simples de controlar a quantidade
        // de arquivos é usando um contador
        fileCount++;

        files[fileCount] = File(
            fileCount,
            _filePath,
            _fileSize,
            _fileType,
            _fileName,
            payable(msg.sender)
        );

        // A partir da aplicação do frontend 
        // podemos listar os eventos emitidos a partir de
        // do smart contract , a fim de atualizar a IU.
        emit FileUploaded(
            fileCount,
            _filePath,
            _fileSize,
            _fileType,
            _fileName,
            payable(msg.sender)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

contract.sol - Visualização em Raw

Implementação de smart contract usando Truffle e Ganache

A maneira mais amigável de programar e testar smart contracts é usar um ambiente de desenvolvimento local como a Truffle para criar facilmente uma boa segmentação de instruções (em inglês, pipeline) para nosso projeto.

Outra ferramenta muito importante para depurar smart contracts é Ganache (Parte da Truffle Suite), que é uma Blockchain local que você pode executar para implantar e depurar seus contratos, eles fornecem um conjunto de contas e configurações fora da caixa com ether suficiente para fazer praticamente tudo o que você quiser em seus projetos.

Portanto, o próximo passo é estabelecer uma truffle em nosso projeto, e para fazê-lo, precisamos simplesmente instalá-lo com npm.

npm install -g truffle

Então você pode criar seu projeto frontend com qualquer framework/library que quiser, no meu caso é o Next.js. Uma vez criado, você pode inicializar um ambiente truffle sobre ele posicionando o console na pasta raiz do projeto e digitando:

truffle init

Isto criará uma configuração básica para seu projeto, entre outros arquivos criará um truffle-config.js, este arquivo será especialmente importante durante o projeto.

Você pode verificar a configuração do projeto aqui.

Para realmente estabelecer nosso smart contract no projeto, devemos realizar uma migração, você pode fazê-lo seguindo estas etapas.

Testando o Smart Contract

Esta parte é uma das práticas mais importantes no desenvolvimento de Smart contracts, um smart contract deve ser testado da melhor forma possível a fim de implementá-lo com confiança. Como sua natureza é imutável, o custo de implantação de um smart contract com bugs é muito alto.

Para testá-lo, a Truffle vem com mocha e chai, o que facilita muito os testes.

Vamos ver alguns exemplos básicos de testes de smart contracts, você pode ver o conjunto completo de testes neste arquivo de testes.

it('deployed successfully', async () => {
    const address = await this.contract.address
    assert.notEqual(address, 0x0)
    assert.notEqual(address, '')
    assert.notEqual(address, null)
    assert.notEqual(address, undefined)
  })
Enter fullscreen mode Exit fullscreen mode

basic_teste_example.js - Visualização em Raw

Testando o upload de um arquivo válido:

 it("uploads a valid file", async () => {
    // checks filecount variable
    // before uploading the file
    const countBeforeUpload = await this.contract.fileCount();

    await this.contract.uploadFile('path', 1, 'type', 'name');

    // checks filecount increased
    // by one after upload
    const countAfterUpload = await this.contract.fileCount();
    assert.equal(Number(countAfterUpload), Number(countBeforeUpload) + 1, "count increased to 1 after upload")

    // checks last file contains the same
    // info than the uploaded file
    const lastFile = await this.contract.files(countAfterUpload);
    assert.equal(lastFile.fileName, 'name', "last file has the filename of the previous uploaded file")
    assert.equal(lastFile.filePath, 'path', "last file has the path of the previous uploaded file")
    assert.equal(lastFile.fileSize, 1, "last file has the size of the previous uploaded file")
    assert.equal(lastFile.fileType, 'type', "last file has the type of the previous uploaded file")

  })
Enter fullscreen mode Exit fullscreen mode

upload_test_example.js - Visualização em Raw

Autenticando com Metamask

Uma vez terminada a configuração básica do aplicativo, podemos começar a trabalhar na autenticação dos usuários com Metamask, para isso, temos uma API js para conectar o aplicativo com a extensão sem a necessidade de qualquer biblioteca.

A fim de criar uma experiência suave para o usuário, podemos criar um método de conexão automática quando houver contas existentes.

async fetchAccounts() {
  if(typeof window === "undefined") return;
  return window
    .ethereum?.request({ method: "eth_accounts" })
    .catch((err: any) => console.log(err));
}
Enter fullscreen mode Exit fullscreen mode

fetch_accounts.js - Visualização em Raw

E outro método para forçar o pedido, abrindo um pop up do Metamask para autenticação.

async requestAccounts() {
  if(typeof window === "undefined") return;
  return window
    .ethereum?.request({ method: "eth_requestAccounts" })
    .catch((err: any) => console.log(err));
}

Enter fullscreen mode Exit fullscreen mode

request_accounts.js - Visualização em Raw

Conecte o Frontend com o Smart Contract

Nesta fase, já temos uma aplicação de frontend conectada com a extensão Metamask e um smart contract em execução com Ganache, agora precisamos encontrar uma maneira de interagir com o contrato _Solidity _como um API.

Para isso, precisaremos de alguma biblioteca como Web3.js ou Ethers.js. No meu caso, eu escolho a Web3.js.

Importando a web3 e criando o objeto do contrato

import Web3 from "web3";

//nós importamos o arquivo abi criado depois
// da migração usando truffle.
import Persssist from '../../public/abis/Persssist.json';


async initializeContractLocal() {
  if (window.ethereum) this.web3 = new Web3(window.ethereum)
  else if (window.web3) this.web3 = new Web3(window.web3.currentProvider);
  if(!this.web3) throw 'Web3 not initialized';

  const networkId = await this.web3.eth.net.getId();
  const networkData = (PersssistLocal as any).networks[networkId];
  if (networkData) {
    this.contract = new this.web3.eth.Contract(
      (Persssist as any).abi,
      networkData.address
    )
  }
}

Enter fullscreen mode Exit fullscreen mode

create_contract.js - Visualização em Raw

Nota: existe um arquivo especial chamado 'Persssist', este é o abi. Uma representação do smart contract em formato JSON que funciona como a interface entre Javascript e Solidity.

Conectando o Smart Contract com o IPFS

Uma vez que tenhamos o objeto do contrato, podemos começar a usar o IPFS para carregar os arquivos. O truque aqui é que podemos fazer referência a um arquivo em IPFS usando o caminho único que funciona como um Id para cada arquivo criado no sistema de arquivos IPFS.

Cada arquivo no estado de smart contract referenciará um caminho no sistema de arquivos IPFS, criando desta forma uma conexão confiável entre o contrato e o sistema de armazenamento.
O único elemento no meio é a aplicação frontend rodando no navegador local para cada usuário, criando desta forma uma aplicação de armazenamento descentralizada.

Upando um arquivo

Esta tarefa é composta por 2 etapas:

  1. Fazer upload do arquivo para o IPFS obter seu caminho único.
  2. Criar o registro do arquivo no smart contract com o resultado.

Primeiro criamos a conexão IPFS, isto não é necessário cada vez que o usuário faz o upload de um arquivo, mas eu o fiz nesta função apenas para tornar o processo mais claro.

Depois carregamos o arquivo apenas usando o buffer e o tipo de arquivo, e como resultado obtemos o caminho do arquivo no sistema de arquivos IPFS.

import { create } from "ipfs-http-client";

async upload(file: File) {
    this.ipfs = create({
      host: 'ipfs.infura.io',
      port: 5001,
      protocol: 'https'
    });

    const blob = new Blob([file.buffer], { type: file.type });
    const result = await this.ipfs.add(blob);

    // the result contains the path
    // to the file on IPFS
    return result;
 }
Enter fullscreen mode Exit fullscreen mode

upload.js - Visualização em Raw

O segundo passo é realmente armazenar as informações do arquivo no smart contract para baixar o arquivo facilmente no futuro.

async uploadFileMetadata(
    path: string, 
    size: number, 
    type: string, 
    name: string, 
    account: string,
  ) {
    return this.contract.methods
        .uploadFile(path, size, type, name)
        .send({ from: account })
        .on('transactionHash', onSuccess)
        .on('error', onError);
  }
Enter fullscreen mode Exit fullscreen mode

upload.js - Visualização em Raw

Busca e download de arquivos

Para esta etapa, vamos fazer o processo inverso. Precisamos buscar os arquivos armazenados no smart contract e depois, usando o caminho único, baixar os arquivos do sistema de arquivos IPFS.

A fim de criar uma interação amigável, podemos ir buscar todos os arquivos e somente baixar os selecionados pelo usuário.

async getFilesMetadata(): Promise<PersssistFile[]> {
        const methods = this.contract.methods;
        const filesCount = await methods.fileCount().call();
        const filesMetadata: PersssistFile[] = [];
        for (var i = filesCount; i >= 1; i--) {
            const file = await methods.files(i).call()
            filesMetadata.push({
                fileId: file.id, 
                fileName: file.fileName, 
                filePath: file.filePath, 
                fileSize: file.fileSize, 
                fileType: file.fileType, 
                uploader: file.uploader
            });
        }
        return filesMetadata;
    }
Enter fullscreen mode Exit fullscreen mode

fetch.js - Visualização em Raw

Depois que o usuário selecionar um arquivo para download, podemos iniciar o processo de download. Esta é uma das partes mais difíceis, porque o IPFS só baixa o arquivo em formato comprimido, então precisamos fazer algum esforço para obter o formato de arquivo correto, neste caso, adicionando a biblioteca untar.

const untar = await require("js-untar");

async download(file: PersssistFile) {
  const iterable = this.ipfs.get(file.filePath);
  var chunks: Uint8Array[] = [];

  // nós precisamos usar for await fazer o download 
  // do buffer em pedaços.
  for await (const b of iterable) {
      chunks.push(b);
  }

// o resultado é um arquivo tar, então precisamos encontrar uma forma de
  // desarmar o arquivo do frontend, no meu caso, eu o fiz com a biblioteca untar.
const tarball = new Blob(chunks, { type: 'application/x-tar' })
  const tarAsArrayBuffer = await tarball.arrayBuffer();
  const result = await this.untar(tarAsArrayBuffer);

 // finalmente podemos criar o blob e fazer o download.
  const resultFile = new Blob([result[0].buffer], { type: file.fileType })
  var url = window.URL.createObjectURL(resultFile);
  this.downloadURL(url, file.fileName);
}
Enter fullscreen mode Exit fullscreen mode

download.js - Visualização em Raw

Conclusão

As aplicações descentralizadas são uma grande oportunidade para capacitar os usuários e desenvolvedores da Internet da web 2.0. Não há necessidade de ter algo no meio de duas pessoas para criar uma interação confiável e fluida, e este é apenas um dos infinitos exemplos da Internet descentralizada que (espero) teremos no futuro próximo.

Se você está aqui, tenho certeza de que adora programar como eu, então recomendo fortemente que você verifique o código e, se ousar, contribua e faça algo maior e melhor a partir dele. Muito obrigado pela leitura!

Nota: Você pode acessar o código do repositório Github (e deixar uma estrela 😉).

Top comments (0)