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);
Podemos então executar este arquivo usando:
DATAHUB_KEY=YOUR_APIKEY node server.js
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;
}
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)
})
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);
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>
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;
}
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;
}
}
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>
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
}
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
}
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>
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;
}
}
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"));
}
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
}
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;
}
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
}
}
}
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)
}
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());
}
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>
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());
}
}
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)
}
...
<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>
! 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
Oldest comments (0)