WEB3DEV

Cover image for Criar Um Widget De Busca De Transação
Banana Labs
Banana Labs

Posted on

Criar Um Widget De Busca De Transação

Aprenda como criar um simples widget de busca de transação usando a API de busca de transação da Figment e o DataHub.

Introdução

A API de busca de transação da Figment permite aos usuários filtrar e consultar por conta, tipo de transação e intervalo de datas. Os usuários também podem buscar por campo de memo e logs. Os desenvolvedores agora podem manipular os dados de transação da maneira que desejarem, em vez da maneira como a blockchain pretendia que eles fizessem.

Este tutorial descreve como criar um widget Javascript simples que mostra as últimas transações para uma determinada conta com filtragem e paginação básicas. Não utilizaremos nenhum framework para nos concentrarmos nas chamadas básicas da API. Você pode encontrar o exemplo de código completo aqui.

! Observe que removemos a chave da API do DataHub dos trechos de código, mas ela será necessária para concluir o tutorial.

Pré-Requisitos

  • Conhecimento básico de Javascript (Fetch API)
  • Conhecimento básico de HTML (templates)
  • Conhecimento básico de CSS (para estilizar o componente)
  • Chave da API do Datahub (que pode ser obtida no Painel de Serviços do Datahub: https://datahub.figment.io/services/cosmos)

Criação De Proxy Do Datahub

! Resumindo, apenas copie e cole e execute o código ao final desta etapa.

Para criar um link entre o aplicativo frontend e o Datahub, precisamos criar um proxy. Precisamos de um proxy para dois propósitos:

  • Ocultar nossa chave da API do DataHub de terceiros
  • Capacidade de fazer solicitações ao DataHub evitando problemas de CORS.

Para manter uma única linguagem neste tutorial, criaremos um aplicativo Nodejs simples que direcionará todas as solicitações para o ponto de extremidade (endpoint) do DataHub.
Primeiro, vamos criar um arquivo server.js com um esquema de arquivo gerenciador (handler) de HTTP simples:

var http = require("http");
var https = require("https");

const PORT = process.env.PORT || 8080;
const DATAHUB_ADDRESS = process.env.DATAHUB_ADDRESS || "cosmos--search.datahub.figment.io";
const DATAHUB_KEY = process.env.DATAHUB_KEY || "";

http.createServer(function(req, res){}).listen(PORT);
Enter fullscreen mode Exit fullscreen mode

Podemos então executar este arquivo usando:

DATAHUB_KEY=YOUR_APIKEY node server.js
Enter fullscreen mode Exit fullscreen mode

Isso executa o serviço em: http://localhost:8080/
Agora, vamos estender a função do servidor com dois trechos. Primeiro, precisamos adicionar a pré-verificação do CORS para solicitações no navegador. Precisamos permitir determinados cabeçalhos (headers), origens e métodos de solicitações diretamente do navegador.

Para o teste local, deve parecer com isto:

res.setHeader("Access-Control-Allow-Origin","*");
res.setHeader("Access-Control-Allow-Methods","POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers","Content-Type");

if (req.method === "OPTIONS") {
 res.writeHead(200);
 res.end();
 return;
}
Enter fullscreen mode Exit fullscreen mode

No caso de uma chamada OPTIONS, precisamos apenas retorná-la com uma resposta HTTP OK, incluindo os cabeçalhos anteriores.
O outro trecho de código é um pouco mais complexo. Ele permite que as solicitações sejam direcionadas de um gerenciador (handler) aberto para outro.

const hReq = https.request(options, (innerRes) => {
    innerRes.headers['content-type'] = 'application/json';
    res.writeHead(innerRes.statusCode, innerRes.headers);

    innerRes.on("error", (error) => {
      res.write(error);
      res.end();
    });
    innerRes.on("data", (data) => {
      res.write(data);
    });
    innerRes.on("end", () => {
      res.end();
      hReq.end();
    });
});

req.on('data', data => {
  hReq.write(data)
})
Enter fullscreen mode Exit fullscreen mode

Juntando tudo isso e adicionando uma solicitação https - o código completo do proxy deve ficar assim:

var http = require("http");
var https = require("https");

const PORT = process.env.PORT || 8080;
const DATAHUB_ADDRESS = process.env.DATAHUB_ADDRESS || "cosmos--search.datahub.figment.io";
const DATAHUB_KEY = process.env.DATAHUB_KEY || "";

http.createServer(function (req, res) {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Methods","POST, OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type");

    if (req.method === "OPTIONS") {
      res.writeHead(200);
      res.end();
      return;
    }

    const options = {
      hostname: DATAHUB_ADDRESS,
      path: req.url,
      method: "POST",
      headers: {
        "accept": "*/*",
        "authorization": DATAHUB_KEY,
        "content-type": "application/json",
        "content-length": req.headers["content-length"],
      },
    };

    const hReq = https.request(options, (innerRes) => {
      innerRes.headers['content-type'] = 'application/json';
      res.writeHead(innerRes.statusCode, innerRes.headers);
      innerRes.on("error", (error) => {
        res.write(error);
        res.end();
      });
      innerRes.on("data", (data) => {
        res.write(data);
      });
      innerRes.on("end", () => {
        res.end();
        hReq.end();
      });
    });

    req.on('data', data => {
        hReq.write(data)
    })

  })
  .listen(PORT); 
Enter fullscreen mode Exit fullscreen mode

Criação Do Esquema Do Aplicativo

O primeiro passo para o aplicativo frontend será criar três arquivos: index.html, style.css e lib.js.

index.html

<!DOCTYPE html>
<html lang="en-us">
<head>
    <script type="text/javascript" async="true" src="./lib.js"></script>
    <link href="./style.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div id="widget-content">
        <h2>Transações</h2>
        <div id="transactions-container"></div>
        <div id="transactions-more">Carregar mais</div>
    </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

style.css

body {
  margin: 0;
  padding: 0;
  font-family: system-ui, sans-serif;
  color: rgb(36, 42, 49);
}

#widget-content {
  display: block;
  width: 50%;
  margin-left: 25%;
}

#widget-content h2 {
  display: block;
  width: 100%;
  background-color: rgb(235, 203, 0);
  padding: 1rem;
}

#transactions-more {
  text-align: right;
  color: rgb(36, 42, 49);
  font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode

lib.js

class SearchRequest {
    constructor(network, limit) {
        this.network = network;
        if (limit === undefined) {
            this.limit = 100;
        } else {
            this.limit = limit;
        }
    }

    stringify() {
        return JSON.stringify(this)
    }

    timeAfter(date) {
        this.after_time = date.toISOString();
    }

    timeBefore(date) {
        this.before_time = date.toISOString();
    } 
    addAccounts(accounts) {
        this.account = accounts;
    }

    addSenders(accounts) {
        this.sender = accounts;
    }

    addReceivers(accounts) {
        this.receiver = accounts;
    }

    addType(types) {
        this.type = types;
    }

    page(number) {
        if (number > 0 ) {
            this.offset = this.limit*number;
        }
    }
}

class Widget {
    /**
     *
     * @param {string} targetID id do div alvo
     * @param {SearchRequest} initialConfig Configuração inicial para o evento onload.
     */
  constructor(targetID, initialConfig) {
    this.targetID = targetID;
    this.initialConfig = initialConfig;

    this.transactions = new Array();
  }

  setRequest(url) {
    this.ApiURL = url;
  }  
}
Enter fullscreen mode Exit fullscreen mode

O arquivo lib.js contém duas classes: Widget, que será nossa classe/controladora principal para o widget, e SearchRequest, que é uma estrutura de dados para contenção de parâmetros de solicitação.

Para verificar se nosso script está funcionando, vamos adicionar um trecho de JavaScript na parte inferior do arquivo index.html:

 ...

   <script type="text/javascript">
        sr = new SearchRequest("cosmos", 30); 
        w = new Widget("transactions-container", sr);
        w.setRequest("http://localhost:8080"); 
   </script>
   </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Solicitação simples.

Agora vamos adicionar a capacidade de solicitação de dados para o widget de transações. Para fazer isso, vamos adicionar alguns métodos à classe Widget:

async fetchData(data) {
    const response = await fetch(this.ApiURL +"/transactions_search", {
        method: 'POST',
        headers: {"Content-Type": "application/json"},
        body: data.stringify(data)
    });

    if (!response.ok) {
      const message = `An error has occurred: ${response.status}`;
      throw new Error(message);
    }

    const list = await response.json();
    return list
  }
Enter fullscreen mode Exit fullscreen mode

Este método é responsável por buscar dados da API com base no objeto de solicitação de busca anterior.
Agora, vamos chamá-lo com os dados de resposta da busca.

 async makeRequest(sr) {
  const response = await this.fetchData(sr)
  if (response === null || response === undefined) {
        return
  }
  for (let i = 0; i < response.length; i++) {
      const tx = response[i];
      if ( !this.transactionsMap.has(tx.id)) {
          tx.dirty = true;
          this.transactionsMap.set(tx.id, tx);
          this.transactions.push(tx);
      }
  }
  this.transactions.sort(compareTransactions);
  this.render();
}

function compareTransactions(a,b) {
    return  b.height-a.height   // na ordem reversa
}
Enter fullscreen mode Exit fullscreen mode

Vamos usar o transactionsMap como filtro para as transações, para não adicionar a mesma transação duas vezes. Podemos fazer isso com base no campo tx.id, que é garantido ser único no escopo da rede.

Renderização

Agora, chamaremos a função de render makeRequest(sr) para que possamos construir o conteúdo do widget. Existem muitas maneiras diferentes de lidar com essa chamada, como ciclos de renderização ou processos de redução de eventos, mas para fins deste tutorial, usaremos a abordagem mais simples.

Para renderizar os dados, usaremos a abordagem mais simples de templates HTML. Portanto, vamos adicionar os templates carregando as seguintes funções:

index.html

<template id="transactionRow">
    <div class="transaction">
        <div class="transaction-head">
            <div class="height"></div>
            <div class="hash"><a></a></div>
            <div class="block"><a></a></div>
            <div class="humanTime"></div>
        </div>
        <em class="memo"></em>
        <div class="events"></div>
    </div>
</template>

<template id="eventRow">
    <div class="event">
        <div class="sub"></div>
    </div>
</template>

<template id="subRow">
    <h3 class="type"></h3>
    <div class="module-element"></div>
</template>

<template id="accountRow">
    <div class="account">
        <div class="id"></div>
        <div class="amount"></div>
    </div>
</template>

<template id="bankElem">
    <div class="senders"><h4></h4></div>
    <div class="recipients"><h4></h4></div>
</template>

<template id="distributionElem">
    <div class="recipients"><h4></h4></div>
    <div class="delegator"><h4></h4></div>
    <div class="validator"><h4></h4></div>
</template>

<template id="stakingElem">
    <div class="params">
        <div class="validator_source"><h4></h4></div>
        <div class="validator_destination"><h4></h4></div>
        <div class="delegator"><h4></h4></div>
        <div class="validator"><h4></h4></div>
    </div>
    <div class="amount"><h4></h4></div>
</template>
Enter fullscreen mode Exit fullscreen mode

Em lib.js, adicionamos a renderização ao Widget juntamente com uma função de ligação.

render() {
    this.linkTemplates();

    let transactionsList = document.querySelector("#"+ this.targetID);
    for (let i = 0; i <  this.transactions.length; i++) {
        if (this.transactions[i].dirty === undefined) {
            continue;
        }

        const elem = createTransactionElem(this.transactions[i], this.templates)
        transactionsList.appendChild(elem);
        this.transactions[i].dirty = undefined;
    }
  }
Enter fullscreen mode Exit fullscreen mode
linkTemplates() {
    if (this.templates.size !== 0) {
        return
    }
    this.templates.set("transactionTemplate", document.querySelector("#transactionRow"));
    this.templates.set("eventsRowTemplate", document.querySelector("#eventRow"));
    this.templates.set("subRowTemplate", document.querySelector("#subRow"));
    this.templates.set("accountRowTemplate", document.querySelector("#accountRow"));

    this.templates.set("bankElemTemplate", document.querySelector("#bankElem"));
    this.templates.set("distributionElemTemplate", document.querySelector("#distributionElem"));
    this.templates.set("stakingElemTemplate", document.querySelector("#stakingElem"));
  }
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, a renderização funciona copiando elementos de modelo e injetando-os nos nós. A seguir, você verá apenas uma parte de todas as funções necessárias para criar a lista. Você pode encontrar o restante do código aqui.

function createTransactionElem(tx, templates) {
    const clone = templates.get("transactionTemplate").content.cloneNode(true);
    const height = clone.querySelector(".height");
    height.innerText = "H: " + shotForm(tx.height, 20);
    const hashA = clone.querySelector(".hash a");
    hashA.innerText = "#: " + shotForm(tx.hash, 20);
    hashA.title = "Hash: " + tx.hash;
    hashA.href = "https://hubble.figment.io/cosmos/chains/"+ tx.chain_id + "/blocks/"+ tx.height + "/transactions/" + tx.hash;

    const time = clone.querySelector(".humanTime");
    time.title =  tx.time;
    time.innerText = humanizeDuration(tx.time, Date.now());
    const a = clone.querySelector(".block a");
    a.href = "https://hubble.figment.io/cosmos/chains/"+ tx.chain_id + "/blocks/"+ tx.height
    a.title = "Block Hash: " + tx.block_hash;
    a.innerText = shotForm(tx.block_hash, 20);

    if (tx.memo  !== undefined) {
        const memo = clone.querySelector(".memo");
        memo.classList.add("filled");
        memo.innerText = "Memo: " +  tx.memo;
    }

    const events = clone.querySelector(".events");
    for (let i = 0; i < tx.events.length; i++) {
        events.appendChild(createEventsElem(tx.events[i],templates));
    }
    return clone
}

function createEventsElem(ev, templates) {
    const clone = templates.get("eventsRowTemplate").content.cloneNode(true);
    const sub = clone.querySelector(".sub");
    for (let i = 0; i < ev.sub.length; i++) {
        sub.appendChild(createSubElem(ev.sub[i] ,templates));
    }
    return clone
}

function createSubElem(sub, templates) {
    const clone = templates.get("subRowTemplate").content.cloneNode(true);
    const kind = clone.querySelector(".type");
    kind.innerText =  sub.module + " / " + sub.type.join(" , ");

    const elm = clone.querySelector(".module-element");
    switch (sub.module) {
        case "bank":
            elm.classList.add("bank");
            elm.appendChild(createBankElem(sub, templates));
        break;
        case "distribution":
            elm.classList.add("distribution");
            elm.appendChild(createDistributionElem(sub, templates));
            break;
        case "staking":
            elm.classList.add("staking");
            elm.appendChild(createStakingElem(sub, templates));
            break;
                // e todos os outros tipos que gostaríamos de suportar
        default:

    }
    return clone
}
Enter fullscreen mode Exit fullscreen mode

Então, adicionamos algumas informações extras ao trecho, mas elas permanecem opcionais.
Primeiro, Vamos reduzir os somatórios dos hashes longos:

/**
 *
 * @param {string} str string para encurtar
 * @param {number} len Comprimento após o qual precisamos encurtar.
 */
function shotForm(str, len) {
    if (str.length > len) {
        return str.substr(0,8) + "..." + str.substr(str.length-8,8);
    }
    return str;
}
Enter fullscreen mode Exit fullscreen mode

Em seguida, formataremos as datas retornadas pelos dados da transação para ficarem mais legíveis. Isso pode mudar ao longo do tempo com a função.

**
 *
 * @param {string} string de tempo com data analisável
 */
function humanizeDuration(time, now) {
    const diff = now - Date.parse(time);
    if (diff > 2592000000) { // ~ a month
        const months = Math.floor(diff / 2592000000)
        return "more than " +  months + ( (months > 1) ? " months ago" : " month ago");
    } else if (diff > 86400000) { // a day
        const days = Math.floor(diff / 86400000)
        return "more than " + days + ( (days > 1) ? " days ago" : " day ago");
    } else if (diff > 3600000) { // an hour
        const hours = Math.floor(diff / 3600000)
        return "more than " + hours + ( (hours > 1) ? " hours ago" : " hour ago");
    } else if (diff > 60000) { // a minute
        const minutes = Math.floor(diff / 60000)
        return  "more than " + minutes + ( (minutes > 1) ? " minutes ago" : " minute ago");
    } else {
        return "less than a minute ago";
    }
}

// e dentro do Widget 

liveDates() {
    return setInterval(this.reformatDates, 10000)
  }

reformatDates() {
    const nodes = document.querySelectorAll(".humanTime");
    let node, hour;
    const now = Date.now()
    for (let i = 0; i < nodes.length; i++) {
        node = nodes[i];
        hour = humanizeDuration(node.getAttribute("title"), now);
        if (hour !== node.innerText) {
            node.innerText = hour
        }
    }
  }
Enter fullscreen mode Exit fullscreen mode

Interação

Para interagir com o widget, os usuários precisarão anexar valores externos ao estado inicial, redefinindo-o.

Inicialização

Na inicialização init do widget, gostaríamos de obter os valores iniciais da transação:

initialRequest() {
    this.makeRequest(this.initialConfig)
  }
Enter fullscreen mode Exit fullscreen mode

Carregar Mais

Em seguida, os dados podem ser paginados armazenando as variáveis de página em uma variável externa e incrementando-a após cada chamada. Também precisamos anexar um gerenciador (handler) de eventos aos elementos HTML apropriados:

loadMoreRequests() {
    const sr = this.initialConfig;
    sr.page(this.lastPage+1);
    this.makeRequest(sr);
    this.lastPage++;
  }

attachEvents() {
    const more = document.querySelector("#transactionsMore");
    more.addEventListener("click", ()=> this.loadMoreRequests());
  }
Enter fullscreen mode Exit fullscreen mode

Escolha O Tipo.

Para anexar um filtro de tipo ao widget, precisamos criar entradas HTML e, em seguida, anexá-las aos eventos de mudança.

<div id="transactions-typebox">
  <div>
    <label><input type="checkbox" name="type" value="send" >send</label>
  </div>

  <div>
    <label><input type="checkbox" name="type" value="begin_unbonding">begin_unbonding</label>
    <label><input type="checkbox" name="type" value="edit_validator">edit_validator</label>
    <label><input type="checkbox" name="type" value="create_validator">create_validator</label>
    <label><input type="checkbox" name="type" value="delegate">delegate</label>
    <label><input type="checkbox" name="type" value="begin_redelegate">begin_redelegate</label>
  </div>

  <div>
    <label><input type="checkbox" name="type" value="withdraw_delegator_reward">withdraw_delegator_reward</label>
    <label><input type="checkbox" name="type" value="withdraw_validator_commission">withdraw_validator_commission</label>
    <label><input type="checkbox" name="type" value="set_withdraw_address">set_withdraw_address</label>
    <label><input type="checkbox" name="type" value="fund_community_pool">fund_community_pool</label>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

A função changeType():

changeType() {
    this.pickedTypes = new Array();
    this.lastPage = 0;

    const checked = document.querySelectorAll('input[name=type]:checked');
    for (let i = 0; i < checked.length; i++) {
        this.pickedTypes.push(checked[i].value);
    }

    // Limpar lista atual
    let transactionsList = document.querySelector("#"+ this.targetID);
    transactionsList.innerHTML = '';

    this.transactions = new Array();
    this.transactionsMap = new Map();

    sr.page(0);
    sr.addType(this.pickedTypes);
    this.makeRequest(sr);
  }

attachEvents() { 

    // ... 

    const types = document.querySelectorAll("input[name=type]");
    for (let i = 0; i < types.length; i++) {
        types[i].addEventListener("change", (ev)=> this.changeType());
    }
  }
Enter fullscreen mode Exit fullscreen mode

Finalmente, precisamos limpar a lista anterior antes de aplicar os filtros e garantir que começamos da primeira página. Isso pode ser feito usando algum mecanismo de cache sofisticado, mas para este tutorial, esta abordagem será suficiente.

Parâmetros Iniciais

Se quisermos que esse widget funcione em um escopo restrito, precisamos passá-lo pela solicitação inicial. Digamos que este widget deva mostrar dados apenas da nossa conta.

Precisamos adicionar um método para as requisições iniciais e modificar o código JavaScript dentro do arquivo index.html.

initialRequest() {
  this.makeRequest(this.initialConfig)
}
Enter fullscreen mode Exit fullscreen mode
...

   <script type="text/javascript">
        sr = new SearchRequest("cosmos", 30); 

                // quaisquer parâmetros iniciais que você desejar

                // Conta específica
                sr.addAccounts("cosmos1fnsu4x447XXXXXXXXXXXXXXqudty");

                // um intervalo de tempo dado
                const now = Date.now();
        sr.timeAfter(new Date(now - (1000 * 3600 * 24 * 7))); // a week ago
        sr.timeBefore(new Date(now));

        w = new Widget("transactions-container", sr);
        w.setRequest("http://localhost:8080"); 
                w.liveDates();
        w.initialRequest();
        w.attachEvents();
   </script>
   </body>
</html>
Enter fullscreen mode Exit fullscreen mode

! Reduzir o intervalo de tempo de busca pode melhorar significativamente o desempenho e permitir que a resposta seja recebida muito mais rapidamente. Nesse caso, ambos os parâmetros (par after_time e before_time ou after_height e before_height) são necessários.

Conclusão

Parabéns, você construiu sua primeira implementação da API de busca de transações. Agora você tem tudo o que precisa para implementá-la em seu DApp e aproveitar todo o poder da busca de transações.
Se ainda não o fez, lembre-se de se inscrever agora para começar a construir em minutos e descobrir os superpoderes que o Datahub pode oferecer!



Esse artigo é uma tradução feita por @bananlabs. Você pode encontrar o artigo original aqui

Latest comments (0)