WEB3DEV

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

Posted on

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

Parte 4 de 4- > Adicionando e removendo liquidez

Acrescentar liquidez

Este vai ser um grande problema, mas um pouco menos complexo do que a troca de tokens!

A parte mais complexa de adicionar liquidez ao contrato Liquidity Baking é acertar a quantidade de tokens! Depois disso, será um passeio no parque.

Primeiro, vamos entender o que estamos fazendo aqui: o LB DEX dá a você a capacidade de fornecer um par de tokens (apenas 2 opções aqui, XTZ e tzBTC) como liquidez para habilitar o recurso de troca. Em troca, recebe tokens SIRS para representar o seu investimento. Esses tokens aumentam de valor ao longo do tempo, portanto, se você esperar o tempo suficiente, poderá obter lucro ao remover sua liquidez, o que será explicado abaixo.

A interface aqui vai se parecer muito com a interface para troca, com algumas diferenças importantes:

Como antes, temos 2 campos de entrada, mas desta vez não há botão do meio para alternar entre os 2 tokens e as duas entradas são editáveis.

Ao inserir um número em um dos campos, o dapp deve calcular a quantidade correspondente do outro token, bem como a quantidade esperada em SIRS que será recebida.

Agora, vamos ver como tudo isso é feito!

Convertendo a entrada

Quando o usuário digita um número em um dos campos, a entrada despacha um novo evento para o componente de interface com o nome do token envolvido e a quantidade inserida. Esses dados serão lidos pela função saveInput:


const saveInput = ev => {

    const { token, val }: { token: token; val: number | null } = ev.detail;

    ...

}

Enter fullscreen mode Exit fullscreen mode

Em seguida, introduziremos uma condição com base no token, porque os cálculos serão diferentes para converter uma quantidade de XTZ em tzBTC e vice-versa. Vamos começar com o XTZ:


if (token === "XTZ" && val && val > 0) {

  inputXtz = val.toString();

  let tzbtcAmount = addLiquidityTokenIn({

    xtzIn: val * 10 ** 6,

    xtzPool: $store.dexInfo.xtzPool,

    tokenPool: $store.dexInfo.tokenPool

  });

  if (tzbtcAmount) {

    inputTzbtc = tzbtcAmount.dividedBy(10 ** 8).toPrecision(6);

  } else {

    inputTzbtc = "";

  }

  ...

}

Enter fullscreen mode Exit fullscreen mode

A condição também inclui uma verificação do valor, pois não há necessidade de processá-lo se o valor for null ou 0.

O valor é convertido em uma sequência e armazenado na variável inputXtz a ser usada posteriormente. O valor correspondente de tzBTC é calculado com a função addLiquidityTokenIn, outra daquelas funções úteis para calcular valores de token diferentes para o LB DEX, aqui está a referência:


const addLiquidityTokenIn = (p: {

  xtzIn: BigNumber | number;

  xtzPool: BigNumber | number;

  tokenPool: BigNumber | number;

}): BigNumber | null => {

  const { xtzIn, xtzPool, tokenPool } = p;

  let xtzIn_ = new BigNumber(0);

  let xtzPool_ = new BigNumber(0);

  let tokenPool_ = new BigNumber(0);

  try {

    xtzIn_ = new BigNumber(xtzIn);

    xtzPool_ = creditSubsidy(xtzPool);

    tokenPool_ = new BigNumber(tokenPool);

  } catch (err) {

    return null;

  }

  if (

    xtzIn_.isGreaterThan(0) &&

    xtzPool_.isGreaterThan(0) &&

    tokenPool_.isGreaterThan(0)

  ) {

    return ceilingDiv(xtzIn_.times(tokenPool_), xtzPool_);

  } else {

    return null;

  }

};

Enter fullscreen mode Exit fullscreen mode

Verificamos a saída de addLiquidityTokenIn e atualizamos a variável inputTzbtc.

Se o usuário inserir um valor em tzBTC, as etapas serão muito semelhantes para calcular o valor correspondente em XTZ:


else if (token === "tzBTC" && val && val > 0) {

  inputTzbtc = val.toString();

  let xtzAmount = tokenToXtzXtzOutput({

    tokenIn: val * 10 ** 8,

    xtzPool: $store.dexInfo.xtzPool,

    tokenPool: $store.dexInfo.tokenPool

  });

  if (xtzAmount) {

    inputXtz = xtzAmount.dividedBy(10 ** 6).toPrecision(8);

    ...

  } else {

    inputXtz = "";

  }

}

Enter fullscreen mode Exit fullscreen mode

Também precisamos verificar se o valor fornecido está correto, depois do que usamos a função tokenToXtzXtzOutput para obter o valor correspondente de XTZ para criar um par válido e fornecer liquidez:


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

Depois que isso é calculado, armazenamos o resultado na variável inputXtz para uso posterior.

Cálculo do valor esperado de SIRS

Agora, temos que calcular a quantidade correspondente de SIRS que será criada se inputXtz e inputTzbtc são fornecidos como parâmetros para adicionar liquidez. A função addLiquidityLiquidityCreated faz todo o trabalho duro para nós:


const addLiquidityLiquidityCreated = (p: {

  xtzIn: BigNumber | number;

  xtzPool: BigNumber | number;

  totalLiquidity: BigNumber | number;

}): BigNumber | null => {

  const { xtzIn, xtzPool, totalLiquidity } = p;

  let xtzIn_ = new BigNumber(0);

  let xtzPool_ = new BigNumber(0);

  let totalLiquidity_ = new BigNumber(0);

  try {

    xtzIn_ = new BigNumber(xtzIn);

    xtzPool_ = new BigNumber(xtzPool);

    totalLiquidity_ = new BigNumber(totalLiquidity);

  } catch (err) {

    return null;

  }

  xtzPool_ = creditSubsidy(xtzPool_);



if (xtzIn_.isGreaterThan(0) && xtzPool_.isGreaterThan(0)) {

  if (totalLiquidity_.isEqualTo(0)) {

    return new BigNumber(xtzIn)

    .times(new BigNumber(totalLiquidity))

    .dividedBy(new BigNumber(xtzPool));

  } else if (totalLiquidity_.isGreaterThan(0)) {

    return new BigNumber(xtzIn)

    .times(new BigNumber(totalLiquidity))

    .dividedBy(new BigNumber(xtzPool));

  }

    return null;

  } else {

    return null;

  }

};

Enter fullscreen mode Exit fullscreen mode

Esta função usa 3 parâmetros:

  1. o valor do XTZ que você deseja adicionar como liquidez

  2. o estado atual do pool XTZ

  3. o montante total de liquidez disponível no contrato ( ou seja, os tokens do SIRS )

Ele produzirá a quantidade de SIRS criada após a transação. Esse valor é armazenado na variável sirsOutput a ser exibida na interface.

Enviando os tokens

Enviando os tokens

Depois de calcularmos todos os valores necessários para adicionar liquidez ao contrato de Liquidity Baking, é hora de forjar a transação!


const addLiquidity = async () => {

  try {

    if (inputXtz && inputTzbtc && sirsOutput) {

      addLiquidityStatus = TxStatus.Loading;

      store.updateToast(

        true,

        "Adding liquidity, waiting for confirmation..."

      );

      const tzbtcForLiquidity = Math.floor(

        +inputTzbtc * 10 ** tzBTC.decimals

      );

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

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

    ...

}

Enter fullscreen mode Exit fullscreen mode

Primeiro, verificamos se os 3 valores que precisamos, as quantidades de XTZ, tzBTC e SIRS estão disponíveis. Se for o caso, atualizamos a interface do usuário alternando a variável addLiquidityStatus para TxStatus.Loading e exibindo um brinde simples com uma mensagem.

Depois disso, convertemos a quantidade de tzBTC que obtivemos em seu valor “real”, ou seja, o valor sem pontos decimais, conforme armazenado em seu contrato.

Então, criamos o ContractAbstraction para o LB DEX e o ContractAbstraction para o contrato tzBTC, pois interagiremos com ambos.

Nota: lembre-se, toda vez que seus usuários desejam usar o tzBTC com o LB DEX, a quantidade de tokens que serão usados precisa ser aprovada no nível do contrato tzBTC, que requer 3 operações diferentes.

Nesse ponto, você pode ter adivinhado que precisamos criar uma transação em lote, mas vamos fazê-lo de uma maneira diferente do capítulo anterior, para que você possa escolher da maneira que preferir:




const batch = $store.Tezos.wallet.batch([

  {

    kind: OpKind.TRANSACTION,

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

  },

  {

    kind: OpKind.TRANSACTION,

    ...tzBtcContract.methods

    .approve(dexAddress, tzbtcForLiquidity)

    .toTransferParams()

  },

  {

    kind: OpKind.TRANSACTION,

    ...lbContract.methodsObject

    .addLiquidity({

      owner: $store.userAddress,

      minLqtMinted: sirsOutput,

      maxTokensDeposited: tzbtcForLiquidity,

      deadline: calcDeadline()

    })

    .toTransferParams(),

    amount: +inputXtz

  },

  {

    kind: OpKind.TRANSACTION,

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

  }

]);



const batchOp = await batch.send();

await batchOp.confirmation();

Enter fullscreen mode Exit fullscreen mode

No capítulo anterior, a transação em lote foi criada usando o método withContractCall disponível no método batch. Aqui, na verdade, passaremos um parâmetro para o método batch(), uma matriz contendo vários objetos que representam uma operação.

A primeira operação:


{

  kind: OpKind.TRANSACTION,

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

}

Enter fullscreen mode Exit fullscreen mode

É a transação necessária para definir o valor do tzBTC aprovado para o LB DEX como zero.

A segunda operação:


{

  kind: OpKind.TRANSACTION,

  ...tzBtcContract.methods

    .approve(dexAddress, tzbtcForLiquidity)

    .toTransferParams()

}

Enter fullscreen mode Exit fullscreen mode

Define o valor do tzBTC aprovado para o contrato LB DEX.

A terceira operação:


{

  kind: OpKind.TRANSACTION,

  ...lbContract

    .methodsObject

    .addLiquidity({

      owner: $store.userAddress,

      minLqtMinted: sirsOutput,

      maxTokensDeposited: tzbtcForLiquidity,

      deadline: calcDeadline()

    })

    .toTransferParams(),

  amount: +inputXtz

}

Enter fullscreen mode Exit fullscreen mode

É a operação addLiquidity para fornecer o par de tokens ao contrato e receber tokens SIRS em troca. O ponto de entrada espera 4 parâmetros ( representados aqui como um objeto graças ao método methodsObject ):

  1. o endereço da conta que receberá os tokens do SIRS

  2. o valor mínimo de tokens SIRS que se espera receber

  3. o montante de tzBTC depositado

  4. o prazo

Nota: veja como a quantidade anexada de tez é passada para a operação como a última propriedade do objeto de operação. É importante colocá-lo depois .toTransferParams() ou seria substituído com a quantidade padrão de tez, que é zero.

A quarta operação:


{

  kind: OpKind.TRANSACTION,

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

}

Enter fullscreen mode Exit fullscreen mode

Redefine a quantidade permitida de tzBTC a ser usada pelo LB DEX para zero.

Então, como qualquer outra transação forjada através do Taquito, você chama o  .send() e .confirmation() no objeto de operação para aguardar uma confirmação.

Depois que a transação for confirmada, você limpará a interface do usuário antes de buscar os novos saldos do XTZ, tzBTC e SIRS.

Se a transação falhar, você atualiza a interface do usuário e fornece feedback visual aos usuários:


addLiquidityStatus = TxStatus.Error;

store.updateToast(true, "An error has occurred");

Enter fullscreen mode Exit fullscreen mode

Depois de todas essas etapas, você pode redefinir a interface para o estado anterior, talvez o usuário queira adicionar mais liquidez!


setTimeout(() => {

  addLiquidityStatus = TxStatus.NoTransaction;

  store.showToast(false);

}, 3000);

Enter fullscreen mode Exit fullscreen mode

E é isso! Seus usuários agora têm a capacidade de adicionar Liquidity Baking DEX e investir seus XTZ e tzBTC.

Remoção de liquidez

A remoção da liquidez do contrato de Liquidity Baking é sem dúvida a mais fácil de todas as tarefas realizadas por nossa interface. A interface precisa apenas de uma entrada para receber a quantidade de SIRS que o usuário deseja desembrulhar para obter XTZ e tzBTC.

O dapp calculará a quantidade correspondente de XTZ e tzBTC que se espera receber pela quantidade de SIRS no campo de entrada.

No arquivo lbUtils.ts, você encontrará a função removeLiquidityXtzTzbtcOut para calcular esses valores:


const outputRes = removeLiquidityXtzTzbtcOut({

  liquidityBurned: val,

  totalLiquidity: $store.dexInfo.lqtTotal.toNumber(),

  xtzPool: $store.dexInfo.xtzPool.toNumber(),

  tokenPool: $store.dexInfo.tokenPool.toNumber()

});

if (outputRes) {

  const { xtzOut, tzbtcOut } = outputRes;

  xtzOutput = xtzOut

    .decimalPlaces(0, 1)

    .dividedBy(10 ** 6)

    .decimalPlaces(6)

    .toNumber();

  tzbtcOutput = tzbtcOut

    .decimalPlaces(0, 1)

    .dividedBy(10 ** 8)

    .decimalPlaces(8)

    .toNumber();

}

Enter fullscreen mode Exit fullscreen mode

Esta função usa um objeto como parâmetro com 4 propriedades:

  • liquidityBurned - > a quantidade de SIRS a queimar

  • totalLiquidity - > o valor total de tokens SIRS no contrato

  • xtzPool - > o valor total de tokens XTZ no contrato

  • tokenPool - > o valor total de tkens tzBTC no contrato

Se a função foi capaz de calcular os valores de XTZ e tzBTC, eles serão retornados em um objeto, caso contrário uma resposta null será devolvida. Depois disso, esses valores podem ser exibidos na interface.

Agora, vamos ver como interagir com o ponto de entrada do contrato removeLiquidity. Primeiro, criamos uma função removeLiquidity dentro do nosso código TypeScript que será acionado quando o usuário clicar no botão Remove liquidity:


const removeLiquidity = async () => {

  try {

  if (inputSirs) {

    removeLiquidityStatus = TxStatus.Loading;

    store.updateToast(

      true,

      "Removing liquidity, waiting for confirmation..."

    );

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

  ...

};

Enter fullscreen mode Exit fullscreen mode

A função começa verificando se há uma quantidade de SIRS que foi inserida antes que a ação de liquidez de remoção fosse acionada. Se for esse o caso, o removeLiquidityStatus está definido para o loading, para atualizar a interface do usuário e informar ao usuário que a transação está se preparando. Um ícone de carregamento também será exibido.

Em seguida, um ContractAbstraction é criado para o LB DEX para interagir com ele do Taquito.

Agora, podemos forjar a transação real:


const op = await lbContract.methodsObject

  .removeLiquidity({

    to: $store.userAddress,

    lqtBurned: inputSirs,

    minXtzWithdrawn: Math.floor(xtzOutput * 10 ** XTZ.decimals),

    minTokensWithdrawn: Math.floor(tzbtcOutput * 10 ** tzBTC.decimals),

    deadline: calcDeadline()

  })

  .send();

await op.confirmation();

Enter fullscreen mode Exit fullscreen mode

O ponto de entrada removeLiquidity espera 5 parâmetros:

  1. to - > a conta que receberá o XTZ e o tzBTC

  2. lqtBurned - > a quantidade de SIRS a queimar

  3. minXtzWithdrawn - > o montante mínimo de XTZ que se espera receber

  4. minTokensWithdrawn - > o montante mínimo de tzBTC que se espera receber

  5. deadline - > assim como o outro ponto de entrada, deve ser fornecido um prazo para a transação

Após a emissão da transação, chamamos o  .confirmation() no objeto de operação retornado por Taquito.

Se a transação foi bem-sucedida, atualizamos a interface do usuário e redefinimos os valores do token para informar o usuário:




removeLiquidityStatus = TxStatus.Success;

inputSirs = "";

xtzOutput = 0;

tzbtcOutput = 0;

// Obtém os saldos do usuário de XTZ, tzBTC e SIRS

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);

}

store.updateToast(true, "Liquidity successfully removed!");

Enter fullscreen mode Exit fullscreen mode

Se a transação falhar, também atualizamos a interface do usuário de acordo:




removeLiquidityStatus = TxStatus.Error;

store.updateToast(true, "An error has occurred");

Enter fullscreen mode Exit fullscreen mode

E é isso, os usuários agora têm a possibilidade de remover tokens SIRS e obter tokens XTZ e tzBTC em troca!

Conclusão

Você chegou até o final deste tutorial 🙂

Esse dapp muito simples introduziu muitos conceitos diferentes que são fundamentais para o desenvolvimento de aplicativos no Tezos, mas também para entender como o Tezos funciona, em geral.

Taquito é uma biblioteca incrível para desenvolver no Tezos, se você deseja criar ideias rapidamente ou deseja criar aplicativos descentralizados de full-stack. Ele fornece uma biblioteca principal com tudo o que você precisa para acessar a blockchain Tezos, interagir com contratos inteligentes e usar carteiras e vários pacotes menores para uso específico, por exemplo, leitura de metadados ou operações de lote.

Se você deseja criar um aplicativo de front-end, back-end ou até um aplicativo de desktop, desde que esteja usando JavaScript / NodeJS, poderá usar o Taquito!

Este tutorial também introduziu diferentes ferramentas que você pode precisar em sua jornada para desenvolver dapps no Tezos, o Beacon SDK para interagir com carteiras, a API TzKT para obter mais dados da blockchain, etc.

Embora este tutorial use o Svelte como framework de escolha, as habilidades que você aprendeu são transferíveis para outros framework, pois são baseadas em muitos dos mesmos conceitos (os ciclos de vida dos componentes são muito semelhantes, etc.) ele oferece tudo o que você precisa para criar incríveis dapps em Tezos e mal posso esperar para ver o que você construirá a seguir!


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)