WEB3DEV

Cover image for Construa um dapp com Tezos (edição 2023) - parte 3
Adriano P. Araujo
Adriano P. Araujo

Posted on • Atualizado em

Construa um dapp com Tezos (edição 2023) - parte 3

Parte 1 aqui

Parte 2 aqui

Parte 4 aqui

Troca de XTZ e tzBTC

Agora, vamos entrar no buraco do coelho e implementar a funcionalidade mais complexa do dapp: a troca de XTZ e tzBTC.

Projetando a interface do usuário

Digo "a mais complexa" porque a interface que você está prestes a construir inclui muitas partes móveis e cálculos que devem ser feitos no momento da entrada e confirmação do usuário. O contrato de Liquidity Baking também é um pouco exigente quanto aos dados que você deve enviar para trocar os tokens, então você terá que ajustar nosso código para garantir que tudo funcione perfeitamente!

Aqui está uma captura da tela da interface com a qual você está trabalhando:

Existem 2 campos de entrada de texto, o da esquerda é editável e permitirá que o usuário insira a quantidade de XTZ ou tzBTC que deseja trocar, e o da direita será desativado e exibirá a quantidade correspondente que eles receberão no outro token. O botão no meio com as duas setas permitirá ao usuário alternar a entrada entre XTZ e tzBTC.

Detalhar como os campos de entrada de texto serão implementados iria além do escopo deste tutorial, mas você pode dar uma olhada no arquivo UserInput.svelte.

Manipulando a entrada do usuário

Resumidamente, cada campo de entrada com o ícone do token e o campo max está no mesmo componente, o componente pai acompanha a posição de cada um para atualizar a interface de usuário de acordo. Internamente, cada componente de entrada acompanha a entrada do usuário e o saldo disponível para exibir mensagens de erro se o saldo for insuficiente. Cada atualização na entrada é enviada para o componente pai para ajustar a interface geral.

Cada vez que uma atualização é enviada para o componente pai (SwapView.svelte), os dados fornecidos com a atualização são passados para a função saveInput:


import {

xtzToTokenTokenOutput,

tokenToXtzXtzOutput,

calcSlippageValue

} from "../lbUtils";




const saveInput = ev => {

const { token, val, insufficientBalance: insufBlnc } = ev.detail;

insufficientBalance = insufBlnc;

if (token === tokenFrom && val > 0) {

inputFrom = val.toString();

inputTo = "";

if (tokenFrom === "XTZ") {

// calcula a quantidade de tzBTC

let tzbtcAmount = xtzToTokenTokenOutput({

xtzIn: val * 10 ** XTZ.decimals,

xtzPool: $store.dexInfo.xtzPool,

tokenPool: $store.dexInfo.tokenPool

});

if (tzbtcAmount) {

inputTo = tzbtcAmount.dividedBy(10 ** tzBTC.decimals).toPrecision(6);

}

// calcula a saída mínima

minimumOutput = calcSlippageValue("tzBTC", +inputTo, +slippage);

} else if (tokenFrom === "tzBTC") {

// calcula a quantidade de XTZ

let xtzAmount = tokenToXtzXtzOutput({

tokenIn: val * 10 ** tzBTC.decimals,

xtzPool: $store.dexInfo.xtzPool,

tokenPool: $store.dexInfo.tokenPool

});

if (xtzAmount) {

inputTo = xtzAmount.dividedBy(10 ** XTZ.decimals).toPrecision(8);

}

// calcula a saída mínima

minimumOutput = calcSlippageValue("XTZ", +inputTo, +slippage);

}

} else {

inputFrom = "";

inputTo = "";

}

};

Enter fullscreen mode Exit fullscreen mode

Aqui, muitas coisas acontecem:

  • Os valores necessários para os cálculos das quantidades de tokens são extraídos do objeto ev.detail.

  • A função verifica se os valores são recebidos do token que está atualmente ativo (o da esquerda).

  • Se esse token for XTZ, a quantidade em tzBTC é calculada por meio da função xtzToTokenTokenOutput (mais detalhes abaixo).

  • Se esse token for tzBTC, a quantidade em XTZ é calculada por meio da função tokenToXtzXtzOutput (mais detalhes abaixo).

  • A quantidade mínima a ser esperada de acordo com a variação aceitável definida pelo usuário é calculada pela função calcSlippage.

Observação: a "variação aceitável" refere-se à porcentagem que o usuário aceita perder durante a negociação, uma perda de tokens pode ocorrer de acordo com o estado das pools de liquidez. Por exemplo, se 100 tokens A puderem ser trocados por 100 tokens B com uma variação aceitável de 1%, significa que você receberá entre 99 e 100 tokens B.

Troca de XTZ por tzBTC e tzBTC por XTZ

Agora, vamos dar uma olhada nas funções que introduzimos acima, xtzToTokenTokenOutput e tokenToXtzXtzOutput. Elas foram adaptadas do código deste repositório e permitem calcular quantos tzBTC um usuário receberá com base na quantidade de XTZ que eles inserirem e vice-versa.


export const xtzToTokenTokenOutput = (p: {

xtzIn: BigNumber | number;

xtzPool: BigNumber | number;

tokenPool: BigNumber | number;

}): BigNumber | null => {

let { xtzIn, xtzPool: xtzPool, tokenPool } = p;

let xtzPool = creditSubsidy(xtzPool);

let xtzIn = new BigNumber(0);

let xtzPool = new BigNumber(0);

let tokenPool_ = new BigNumber(0);

try {

xtzIn_ = new BigNumber(xtzIn);

xtzPool_ = new BigNumber(xtzPool);

tokenPool_ = new BigNumber(tokenPool);

} catch (err) {

return null;

}

if (

xtzIn_.isGreaterThan(0) &&

xtzPool_.isGreaterThan(0) &&

tokenPool_.isGreaterThan(0)

) {

const numerator = xtzIn_.times(tokenPool_).times(new BigNumber(998001));

const denominator = xtzPool_

.times(new BigNumber(1000000))

.plus(xtzIn_.times(new BigNumber(998001)));

return numerator.dividedBy(denominator);

} else {

return null;

}

};

Enter fullscreen mode Exit fullscreen mode

A função xtzToTokenTokenOutput requer 3 valores para calcular uma saída em tzBTC a partir de uma entrada em XTZ: a quantidade mencionada em XTZ (xtzIn), o estado da pool de XTZ no contrato (xtzPool) e o estado da pool de SIRS (tokenPool). A maioria das modificações feitas nas funções originais se refere ao uso do BigNumber para que funcione de forma mais suave possível com o Taquito. A função retorna a quantidade correspondente em tzBTC ou null, se ocorrer um erro.

O mesmo se aplica a tokenToXtzXtzOutput:


export const tokenToXtzXtzOutput = (p: {

tokenIn: BigNumber | number;

xtzPool: BigNumber | number;

tokenPool: BigNumber | number;

}): BigNumber | null => {

const { tokenIn, xtzPool: xtzPool, tokenPool } = p;

let xtzPool = creditSubsidy(xtzPool);

let tokenIn = new BigNumber(0);

let xtzPool = new BigNumber(0);

let tokenPool_ = new BigNumber(0);

try {

tokenIn_ = new BigNumber(tokenIn);

xtzPool_ = new BigNumber(xtzPool);

tokenPool_ = new BigNumber(tokenPool);

} catch (err) {

return null;

}

if (

tokenIn_.isGreaterThan(0) &&

xtzPool_.isGreaterThan(0) &&

tokenPool_.isGreaterThan(0)

) {

let numerator = new BigNumber(tokenIn)

.times(new BigNumber(xtzPool))

.times(new BigNumber(998001));

let denominator = new BigNumber(tokenPool)

.times(new BigNumber(1000000))

.plus(new BigNumber(tokenIn).times(new BigNumber(999000)));

return numerator.dividedBy(denominator);

} else {

return null;

}

};

Enter fullscreen mode Exit fullscreen mode

Após calcular a quantidade correspondente de XTZ ou tzBTC com base nas entradas do usuário, a interface é desbloqueada e está pronta para a troca.

Criando uma transação de troca

A parte de troca de tokens é bastante intensiva, pois envolve vários pontos móveis que devem funcionar em conjunto. Vamos descrever passo a passo o que acontece após o usuário clicar no botão Swap:


const swap = async () => {

try {

if (isNaN(+inputFrom) || isNaN(+inputTo)) {

return;

}



...

} catch (error) {

console.log(error);

swapStatus = TxStatus.Error;

store.updateToast(true, "Ocorreu um erro");

}

};

Enter fullscreen mode Exit fullscreen mode

A função swap é acionada quando o usuário clica no botão Swap. A primeira coisa a fazer é verificar se há um valor válido para inputFrom, ou seja, se o token que o usuário deseja trocar (XTZ ou tzBTC), é um valor válido para inputTo, em outras palavras, o token que o usuário receberá. Não faz sentido prosseguir se esses dois valores não estiverem definidos corretamente.

Em seguida, você atualiza a interface do usuário para mostrar que a transação está sendo preparada:


enum TxStatus {

NoTransaction,

Loading,

Success,

Error

}




swapStatus = TxStatus.Loading;

store.updateToast(true, "Aguardando confirmação da troca...");

const lbContract = await $store.Tezos.wallet.at(dexAddress);

const deadline = calcDeadline();

Enter fullscreen mode Exit fullscreen mode

Você cria um enum para representar o status da transação (disponível no arquivo type.ts) e atualiza a variável swapStatus responsável por atualizar a interface do usuário e bloquear as entradas. A store também é atualizada com o método updateToast() para exibir uma mensagem simples na interface.

Depois disso, você cria o ContractAbstraction do Taquito para interagir com o DEX e também calcula o prazo de vencimento (deadline).

Observação: o contrato Liquidity Baking espera que você passe um prazo de vencimento para a troca, e a transação será rejeitada se o prazo expirar.

Troca de tzBTC por XTZ

Agora, existem duas situações: o usuário selecionou XTZ ou tzBTC como o token para a troca. Vamos começar com tzBTC, pois a preparação da transação é mais complicada:


if (tokenFrom === "tzBTC") {

const tzBtcContract = await $store.Tezos.wallet.at(tzbtcAddress);

const tokensSold = Math.floor(+inputFrom * 10 ** tzBTC.decimals);

let batch = $store.Tezos.wallet

.batch()

.withContractCall(tzBtcContract.methods.approve(dexAddress, 0))

.withContractCall(

tzBtcContract.methods.approve(dexAddress, tokensSold)

)

.withContractCall(

lbContract.methods.tokenToXtz(

$store.userAddress,

tokensSold,

minimumOutput,

deadline

)

)

.withContractCall(tzBtcContract.methods.approve(dexAddress, 0));

const batchOp = await batch.send();

await batchOp.confirmation();

}

Enter fullscreen mode Exit fullscreen mode

A principal diferença entre trocar XTZ por tzBTC e trocar tzBTC por XTZ é que esta última requer 3 operações adicionais: uma para definir a permissão atual do LB DEX (se houver) para zero, uma para registrar o LB DEX como operador dentro do contrato tzBTC com a quantidade de tokens que ele tem permissão para gastar em nome do usuário, e uma para definir essa quantidade de volta para zero e evitar o uso indevido dessa permissão posteriormente.

_Observação 1: você pode ler mais sobre os comportamentos do contrato tzBTC e outros contratos FA1.2 aqui.

Observação 2: tecnicamente, não é necessário definir a permissão como zero no final da transação (mas é necessário definir como zero no início). É apenas uma prática comum para evitar qualquer permissão pendente inesperada._

Primeiro, você cria o ContractAbstraction para o contrato tzBTC, pois você está prestes a interagir com ele. Feito isso, você calcula a quantidade de tokens que deve aprovar com base nos cálculos anteriores.

Observação: o ContractAbstraction é uma instância muito útil fornecida pelo Taquito, que expõe diferentes ferramentas e propriedades para obter detalhes sobre um contrato específico ou interagir com ele.

Depois disso, você usa a API Batch  fornecida pelo Taquito. A API  Batch permite agrupar várias operações em uma única transação, para economizar gás e tempo de processamento. Veja como funciona:

  1. Você chama o método batch() presente na propriedade wallet ou contract da instância do TezosToolkit.

  2. Isso retorna uma instância batch com diferentes métodos que você pode usar para criar transações. No nosso exemplo, o método withContractCall() adicionará uma nova chamada de contrato ao conjunto de operações batch.

  3. Como parâmetro para withContractCall(), você passa a chamada do contrato, como se estivesse chamando-o individualmente, usando o nome do ponto de entrada (entrypoint) na propriedade methods do ContractAbstraction.

  4. Neste caso, você agrupa 1 operação para definir a permissão do LB DEX dentro do contrato tzBTC como zero, 1 operação para aprovar a quantidade necessária para a troca, 1 operação para confirmar a troca dentro do contrato LB DEX e 1 operação para definir a permissão do LB DEX de volta para zero.

  5. No batch retornado, você chama o método .send() para forjar a transação, assiná-la e enviá-la para a mempool da Tezos, o que retorna uma operação.

  6. Você pode aguardar (await) a confirmação da transação chamando .confirmation() na operação retornada no passo anterior.

Observe a penúltima transação: o ponto de entrada tokenToXtz do contrato LB requer 4 parâmetros:

  • O endereço da conta que receberá o XTZ.

  • A quantidade de tzBTC que será vendida na troca.

  • A quantidade esperada de XTZ que será recebida.

  • Um prazo de validade após o qual a transação expira.

Após o envio da transação chamando o método .send(), você chama .confirmation() na operação para aguardar uma confirmação (o valor padrão é uma confirmação).

Trocar XTZ por tzBTC

Isso será muito mais fácil! Vamos verificar o código primeiro:


const op = await lbContract.methods

.xtzToToken($store.userAddress, minimumOutput, deadline)

.send({ amount: +inputFrom });

await op.confirmation();

Enter fullscreen mode Exit fullscreen mode

O ponto de entrada xtzToToken recebe 3 parâmetros:

  • O endereço da conta que receberá os tokens tzBTC.

  • A quantidade esperada de tzBTC a ser recebida.

  • O prazo de validade.

Além disso, você precisa anexar a quantidade correta de XTZ à transação. Isso pode ser feito facilmente com o Taquito.

Lembra do método .send() que você chama na saída da chamada do ponto de entrada? Se você não sabia, você pode passar parâmetros para esse método. Um dos mais importantes é a quantidade de XTZ a ser enviada junto com a transação. Basta passar um objeto com a propriedade amount e um valor correspondente à quantidade de tez que deseja anexar, e pronto!

Depois disso, assim como qualquer outra transação, você obtém um objeto e chama o .confirmation() nele para aguardar a inclusão da operação em um novo bloco.

Atualizando a interface do usuário

Se a troca for bem-sucedida, você buscará os novos saldos do usuário e fornecerá um feedback visual:


const res = await fetchBalances($store.Tezos, $store.userAddress);

if (res) {

store.updateUserBalance("XTZ", res.xtzBalance);

store.updateUserBalance("tzBTC", res.tzbtcBalance);

store.updateUserBalance("SIRS", res.sirsBalance);

} else {

store.updateUserBalance("XTZ", null);

store.updateUserBalance("tzBTC", null);

store.updateUserBalance("SIRS", null);

}

// Feedback visual

store.updateToast(true, "Troca bem-sucedida!");

Enter fullscreen mode Exit fullscreen mode

Observação: também seria possível evitar 2 requisições HTTP e calcular os novos saldos com base nas quantidades passadas como parâmetros para a troca. No entanto, os usuários podem ter recebido tokens desde a última vez que os saldos foram buscados, e oferecerá uma melhor experiência ao usuário se você obtiver os saldos precisos após a troca.

Se a troca não for bem-sucedida, você será redirecionado para o bloco catch, onde também deverá fornecer feedback visual e atualizar a interface do usuário:


swapStatus = TxStatus.Error;

store.updateToast(true, "Ocorreu um erro");

Enter fullscreen mode Exit fullscreen mode

Ao definir swapStatus como TxStatus.Error removerá a interface de carregamento que você definiu durante a troca antes de exibir uma mensagem de alerta para indicar que a transação falhou.

Finalmente (sem trocadilhos), você passa para o bloco finally(finalmente) e  redefine a interface do usuário após 3 segundos:


finally {

setTimeout(() => {

swapStatus = TxStatus.NoTransaction;

store.showToast(false);

}, 3000);

}

Enter fullscreen mode Exit fullscreen mode

Considerações de design

Como você pode ver no código envolvido, trocar tokens é uma ação bastante complexa e há algumas coisas que você deve ter em mente, tanto em relação ao código que você escreve quanto à interface do usuário que você cria:

  • Tente estruturar seu código em diferentes etapas que não se misturem, por exemplo, etapa 1: atualizar a interface do usuário antes de forjar a transação, etapa 2:: forjar a transação, etapa 3: emitir a transação, etapa 4: atualizar a interface do usuário, etc.

  • Nunca se esqueça de fornecer feedback visual aos seus usuários! Fazer uma nova operação pode levar até 30 segundos quando a rede não está congestionada, e até mais tempo se houver muito tráfego. Os usuários ficarão se perguntando o que está acontecendo se você não os fizer esperar. Um spinner ou uma animação de carregamento é geralmente uma boa ideia para indicar que o aplicativo está aguardando alguma confirmação.

  • Desative a interface do usuário enquanto a transação estiver na mempool ! Você não quer que os usuários cliquem no botão troca uma segunda vez (ou terceira, ou quarta!) enquanto a blockchain estiver processando a transação que eles já criaram. Além de custar mais dinheiro, isso também pode confundi-los e criar comportamentos inesperados em sua interface do usuário.

  • Redefina a interface do usuário no final. Ninguém quer clicar no botão  atualizar após uma interação com a blockchain porque a interface parece estar presa em seu estado anterior. Certifique-se de que a interface esteja no mesmo (ou em um estado semelhante) ao que estava quando o usuário a abriu pela primeira vez.

Na quarta parte, adicionando e removendo liquidez =>


Este artigo foi escrito por  Claude Barde e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.

Latest comments (0)