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 = "";
}
};
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;
}
};
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;
}
};
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");
}
};
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();
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();
}
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:
Você chama o método
batch()
presente na propriedadewallet
oucontract
da instância doTezosToolkit
.Isso retorna uma instância
batch
com diferentes métodos que você pode usar para criar transações. No nosso exemplo, o métodowithContractCall()
adicionará uma nova chamada de contrato ao conjunto de operações batch.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 propriedademethods
do ContractAbstraction.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.
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.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();
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!");
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");
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);
}
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)