Часть 3 из 4-› Обмен токенами

Обмен XTZ и tzBTC

Теперь давайте спустимся в кроличью нору и реализуем самую сложную функцию децентрализованного приложения: обмен XTZ и tzBTC.

Разработка пользовательского интерфейса

Я говорю «наиболее сложный», потому что интерфейс, который вы собираетесь построить, включает в себя множество движущихся частей и вычислений, которые должны быть выполнены в момент ввода и подтверждения пользователем. Контракт Liquidity Baking также немного требователен к данным, которые вы должны отправить для обмена токенов, поэтому вам придется настроить наш код, чтобы убедиться, что он работает как часы!

Вот скриншот пользовательского интерфейса, к которому вы стремитесь:

Есть 2 текстовых ввода, тот, что слева, доступен для редактирования и позволяет пользователю вводить сумму XTZ или tzBTC, которую он хочет обменять, а тот, что справа, будет отключен и будет отображать соответствующую сумму, которую они получат в обмене. другой жетон. Кнопка посередине с двумя стрелками позволит пользователю переключать ввод между XTZ и tzBTC.

Подробное рассмотрение того, как реализован ввод текста, выходит за рамки этого руководства, но вы можете посмотреть его в файле UserInput.svelte.

Обработка пользовательского ввода

В двух словах, каждый вход со своим значком маркера и полем max является одним и тем же компонентом, родительский компонент отслеживает позицию каждого, чтобы соответствующим образом обновить свой пользовательский интерфейс. Внутри каждый компонент ввода отслеживает ввод пользователя и доступный баланс для отображения сообщений об ошибках, если баланс слишком низкий. Каждое обновление на входе отправляется родительскому компоненту для настройки общего пользовательского интерфейса.

Каждый раз, когда обновление отправляется родительскому компоненту (SwapView.svelte), данные, предоставленные вместе с обновлением, передаются функции 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") {
      // calculates tzBTC amount
      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);
      }
      // calculates minimum output
      minimumOutput = calcSlippageValue("tzBTC", +inputTo, +slippage);
    } else if (tokenFrom === "tzBTC") {
      // calculates XTZ amount
      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);
      }
      // calculates minimum output
      minimumOutput = calcSlippageValue("XTZ", +inputTo, +slippage);
    }
  } else {
    inputFrom = "";
    inputTo = "";
  }
};

Тут много чего происходит:

  • значения, необходимые для расчета количества токенов, деструктурированы из объекта ev.detail
  • функция проверяет, что значения получены от токена, который в данный момент активен (тот, что слева)
  • если этот токен XTZ, сумма в tzBTC рассчитывается с помощью функции xtzToTokenTokenOutput (подробнее об этом ниже)
  • если этот токен tzBTC, сумма в XTZ рассчитывается с помощью функции tokenToXtzXtzOutput (подробнее об этом ниже)
  • минимальная сумма, ожидаемая в соответствии с установленным пользователем проскальзыванием, рассчитывается функцией calcSlippage

Примечание: «проскальзывание» относится к проценту, который пользователь соглашается потерять во время торговли, потеря токенов может произойти в зависимости от состояния пулов ликвидности. Например, если 100 токенов A можно обменять на 100 токенов B с проскальзыванием 1%, это означает, что вы получите от 99 до 100 токенов B.

Обмен XTZ на tzBTC и tzBTC на XTZ

Теперь давайте посмотрим на функции, которые мы представили выше, xtzToTokenTokenOutput и tokenToXtzXtzOutput. Они были адаптированы из кода в этом репозитории и позволяют вам рассчитать, сколько tzBTC получит пользователь в соответствии с введенной им суммой XTZ, и наоборот.

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

Функция xtzToTokenTokenOutput требует 3 значения для расчета вывода в tzBtc из ввода в XTZ: указанная сумма в XTZ (xtzIn), состояние пула XTZ в контракте (xtzPool) и состояние пула SIRS (tokenPool). Большинство модификаций, внесенных в исходные функции, относятся к использованию BigNumber, чтобы сделать его работу с Taquito более гладкой. Затем функция возвращает соответствующую сумму в tzBTC или null в случае ошибки.

То же самое касается 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;
  }
};

После того, как соответствующее количество XTZ или tzBTC будет рассчитано в соответствии с вводом пользователя, пользовательский интерфейс разблокируется и будет готов к обмену.

Создание сделки своп

Замена жетонов довольно трудоемка, поскольку они представляют собой несколько движущихся частей, которые должны играть в унисон. Давайте шаг за шагом опишем, что происходит после того, как пользователь нажимает кнопку Swap:

const swap = async () => {
  try {
    if (isNaN(+inputFrom) || isNaN(+inputTo)) {
      return;
    }
  
  ...
  } catch (error) {
    console.log(error);
    swapStatus = TxStatus.Error;
    store.updateToast(true, "An error has occurred");
  }
};

Функция swap запускается, когда пользователь нажимает кнопку Поменять местами. Первое, что нужно сделать, это проверить, существует ли допустимое значение для inputFrom, то есть токена, который пользователь хочет обменять (XTZ или tzBTC), и допустимое значение для inputTo, то есть токен, который получит пользователь. Нет смысла идти дальше, если эти два значения не установлены должным образом.

Затем вы обновляете пользовательский интерфейс, чтобы показать пользователю, что транзакция готовится:

enum TxStatus {
  NoTransaction,
  Loading,
  Success,
  Error
}

swapStatus = TxStatus.Loading;
store.updateToast(true, "Waiting to confirm the swap...");
const lbContract = await $store.Tezos.wallet.at(dexAddress);
const deadline = calcDeadline();

Вы создаете enum для представления статуса транзакции (доступно в файле type.ts) и обновляете переменную swapStatus, отвечающую за обновление пользовательского интерфейса и блокировку входных данных. В магазин также добавлен метод updateToast(), чтобы в интерфейсе отображалось простое всплывающее уведомление.

После этого вы создаете ContractAbstraction от Taquito для взаимодействия с DEX, а также рассчитываете крайний срок.

Примечание: контракт на выпечку ликвидности предполагает, что вы пройдете крайний срок для свопа, транзакция будет отклонена, если крайний срок истечет.

Обмен tzBTC на XTZ

Теперь у вас есть 2 ситуации: пользователь выбрал XTZ или tzBTC в качестве токена для обмена. Начнем с tzBTC, так как подготовка транзакции более сложная:

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

Основное различие между обменом XTZ на tzBTC и обменом tzBTC на XTZ заключается в том, что последний требует 3 дополнительных операций: одна для установки текущего разрешения для LB DEX (если есть) на ноль, одна для регистрации LB DEX в качестве оператора в пределах tzBTC заключает контракт с количеством токенов, которое разрешено тратить от имени пользователя, и одним, чтобы обнулить эту сумму и избежать дальнейшего использования данного разрешения.

Примечание 1: вы можете узнать больше о поведении контракта tzBTC и других контрактов FA1.2 здесь.

Примечание 2: с технической точки зрения нет необходимости устанавливать разрешение обратно на ноль в конце транзакции (но необходимо установить его на ноль в начале). Это просто обычная практика для предотвращения любых неожиданных ожидающих разрешений.

Во-первых, вы создаете ContractAbstraction для контракта tzBTC, когда собираетесь взаимодействовать с ним. После этого вы рассчитываете количество токенов, которое вы должны одобрить, на основе наших предыдущих расчетов.

Примечание: ContractAbstraction — это очень полезный экземпляр, предоставленный Taquito, который предоставляет различные инструменты и свойства для получения подробной информации о данном контракте или взаимодействия с ним.

После этого вы используете Batch API, предоставленный Taquito. Пакетный API позволяет объединять несколько операций в одну транзакцию, чтобы сэкономить газ и время обработки. Вот как это работает:

  1. Вы вызываете метод batch(), присутствующий в свойстве wallet или contract экземпляра TezosToolkit.
  2. Это возвращает пакетный экземпляр с различными методами, которые вы можете использовать для создания транзакций, в нашем примере withContractCall() — это метод, который добавит новый вызов контракта в пакет операций.
  3. В качестве параметра для withContractCall() вы передаете вызов контракта, как если бы вы вызывали его самостоятельно, используя имя точки входа в свойстве methods объекта ContractAbstraction.
  4. В этом случае вы выполняете 1 операцию, чтобы установить разрешение LB DEX в рамках контракта tzBTC на ноль, 1 операцию для утверждения суммы, необходимой для свопа, 1 операцию для подтверждения свопа в рамках контракта LB DEX и 1 операцию для установить разрешение LB DEX обратно на ноль
  5. В возвращенном пакете вы вызываете метод .send() для подделки транзакции, подписываете ее и отправляете в мемпул Tezos, который возвращает операцию
  6. Вы можете await подтвердить транзакцию, вызвав .confirmation() операцию, возвращенную на шаге выше.

Обратите внимание на предпоследнюю транзакцию: tokenToXtz точка входа LB-контракта требует 4 параметра:

  • Адрес учетной записи, которая получит XTZ
  • Количество tzBTC, которое будет продано за своп
  • Ожидаемая сумма XTZ, которая будет получена
  • Крайний срок, после которого транзакция истекает

После отправки транзакции путем вызова метода .send() вы вызываете .confirmation() для объекта операции, чтобы дождаться одного подтверждения (это значение по умолчанию, если вы не передаете параметр методу).

Обмен XTZ на tzBTC

Это будет намного проще! Давайте сначала проверим код:

const op = await lbContract.methods
  .xtzToToken($store.userAddress, minimumOutput, deadline)
  .send({ amount: +inputFrom });
await op.confirmation();

Точка входа xtzToToken принимает 3 параметра:

  • Адрес учетной записи, которая получит токены tzBTC
  • Ожидаемое количество tzBTC, которое будет получено
  • Последний срок

Кроме того, вы должны привязать к транзакции нужное количество XTZ. Это может быть достигнуто очень легко с Taquito.

Помните метод .send(), который вы вызываете на выходе точки входа? Если вы не знали, вы можете передать параметры этому методу, один из самых важных — количество XTZ для отправки вместе с транзакцией. Просто передайте объект со свойством amount и значением количества tez, которое вы хотите прикрепить, и все!

Затем, как и в любой другой транзакции, вы получаете объект операции и вызываете для него .confirmation(), чтобы дождаться включения операции в новый блок.

Обновление пользовательского интерфейса

Независимо от того, был ли обмен успешным или нет, крайне важно предоставить обратную связь вашим пользователям.

Если замена прошла успешно, вы получите новые балансы пользователя и предоставите визуальную обратную связь:

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);
}
// visual feedback
store.updateToast(true, "Swap successful!");

Примечание: также можно было бы избежать 2 HTTP-запросов и рассчитать новые балансы из сумм, которые были переданы в качестве параметров для свопа. Тем не менее, пользователи могли получить токены с момента последнего получения балансов, и это обеспечит лучший пользовательский опыт, если вы получите точные балансы после обмена.

Если замена не удалась, вы будете перенаправлены в ветку catch, где вы также должны предоставить визуальную обратную связь и обновить пользовательский интерфейс:

swapStatus = TxStatus.Error;
store.updateToast(true, "An error has occurred");

Установка swapStatus на TxStatus.Error удалит интерфейс загрузки, который вы установили во время обмена, прежде чем отобразится всплывающее уведомление, указывающее, что транзакция не удалась.

Наконец (каламбур) вы переходите к ветке finally, чтобы сбросить пользовательский интерфейс через 3 секунды:

finally {
  setTimeout(() => {
    swapStatus = TxStatus.NoTransaction;
    store.showToast(false);
  }, 3000);
}

Соображения по дизайну

Как видно из используемого кода, замена токенов — довольно сложное действие, и есть несколько вещей, которые вы должны иметь в виду, как в отношении кода, который вы пишете, так и в отношении создаваемого вами пользовательского интерфейса:

  • Попробуйте структурировать свой код на разные шаги, которые не смешиваются, например, шаг 1: обновление пользовательского интерфейса перед подделкой транзакции, шаг 2: подделка транзакции, шаг 3: отправка транзакции, шаг 4: обновление пользовательского интерфейса и т. д. .
  • Никогда не забывайте предоставлять визуальную обратную связь своим пользователям! Запекание новой операции может занять до 30 секунд, если сеть не перегружена, и даже больше, если трафик большой. Пользователи будут недоумевать, что происходит, если вы не заставите их ждать. Спиннер или анимация загрузки, как правило, являются хорошей идеей, чтобы указать, что приложение ожидает какого-либо подтверждения.
  • Отключите пользовательский интерфейс, пока транзакция находится в мемпуле! Вы не хотите, чтобы пользователи нажимали кнопку Обмен второй раз (или третий, или четвертый!), пока блокчейн обрабатывает уже созданную ими транзакцию. Помимо того, что это будет стоить им больше денег, это также может сбить их с толку и создать неожиданное поведение в вашем пользовательском интерфейсе.
  • Сбросьте пользовательский интерфейс в конце. Никто не хочет нажимать кнопку Обновить после взаимодействия с блокчейном, потому что пользовательский интерфейс, кажется, застрял в своем предыдущем состоянии. Убедитесь, что интерфейс находится в том же (или подобном) состоянии, в котором он был, когда пользователь впервые открыл его.

В четвертой части добавление и удаление ликвидности =›

Новичок в трейдинге? Попробуйте криптотрейдинговые боты или копи-трейдинг на лучших криптобиржах