Введение

Здравствуйте!
В этой статье мы поговорим о том, что такое транзакция и как отправлять ее в Ethereum совместимых блокчейнах. Чуть ниже вы можете ознакомиться с содержанием и выбрать интересующий вас раздел.

Кому подойдет статья?


Если вы хорошо владеете javascript и знакомы с основными сущностями ethers.js (Contract, Provider) или прочли мою статью Ethers js - Основы, то можете смело читать дальше. В другом случае материал может быть сложен для восприятия.

Содержание

  • Сущность транзакция

  • Отправка транзакции

  • Покупка ERC-721 токена

  • Обмен ERC-20 токенов

Сущность транзакция

Транзакция - это запись в блокчейне, которая обычно содержит информацию о переводе криптовалюты или любых других данных от одного участника сети к другому. Другими словами методы , которые осуществляют запись каких-либо данных в блокчейне.


Транзакции используются для передачи активов, подтверждения прав собственности, голосования, взаимодействия со смарт-контрактами и других операций в блокчейне.

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

В общем виде транзакция Ethereum состоит из нескольких ключевых элементов:

  • from - адрес кошелька, откуда идет транзакция

  • to - адрес кошелька, куда направляется транзакция

  • value - количество ETH, которые отправляются в транзакции.

  • gasPrice - количество ETH, которое отправитель платит за обработку транзакции узлами сети

  • blockNumber - номер блока, в котором будет включена транзакция (если номер блока не указан, то транзакция попадет в первый свободный блок)

  • signer - цифровая подпись, которая подтверждает авторство транзакции отправителем.

После обработки транзакции появляются дополнительные поля:

  • hash - уникальный идентификатор

  • status - статус транзакция (pending, failed, success)

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

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

Отправка транзакции

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

Условно можно выделить 2 случая:

  • Метод смарт-контракта принимает только ETH или не требует оплаты и необходима только плата за обработку транзакции

  • Метод смарт-контракта имеет гибкую структуру, опционально может принимать токены определенного стандарта, ETH и/или ETH в качестве оплаты за обработку транзакции

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

Поэтому для начала объявим переменные для провайдеров и реализуем функцию connect:

import { ethers } from 'ethers'

let web3 = null
let provider = null

async connect () {
    // проверяем есть ли кошелек
    if (window.ethereum) { 
      // сохраняем экземпляр для дальнейшей инициализации web3Provider
      provider = window.ethereum 
      try {
        // получаем учетные записи кошелька
        await window.ethereum.request({ method: 'eth_requestAccounts' })
      } catch (e) {
        console.error(e)
        if (e.code === 4001) return
      }
    } else if (window.web3) {
      provider = window.web3.currentProvider
    }
    if (provider) {
        // инициалзируем web3 provider
        web3 = new ethers.providers.Web3Provider(provider, 'any')
      }
  }

В примере кода мы инициализируем экземпляры провайдера, необходимые для взаимодействия со смарт-контрактами.

В случаях рассмотренных ниже мы будем считать метод connect вызванным, а следовательно провайдеры уже известными.

Покупка ERC-721 токена


Ярким примером смарт-контракта, который ожидает получить только ETH является смарт-контракта ERC-721 токена.

ERC-721 обозначает стандарт токена который реализует определенные методы.

В нашем случае мы будем работать с методом mint, который принимает ETH в качестве оплаты, выпускает ERC-721 токен и отправляет его на адрес отправителя.

Как вы могли заметить метод mint именно выпускает токен, поэтому термин покупка не совсем корректен и использовался для простоты восприятия.

Рассмотрим как выпустить ERC-721 на примере NFT-коллекции FrankenPunks в сети Ethereum.

Реализуем функцию mint, которая и будет отправлять ETH и выпускать отправителю ERC-721 токен:

import { ethers } from "ethers"
// адрес смарт-контракта
const CONTRACT_ADDRESS = "0x1FEC856e25F757FeD06eB90548B0224E91095738"
// описание методов смарт-контракта
const ABI = [
  {
      // описание аргументов метода (кол-во выпускаемых ERC-721)
      "inputs": [
        {
          "internalType": "uint256",
          "name": "numToMint",
          "type": "uint256"
        }
      ],
      "name": "getCost",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      // view означает что выполняет чтение
      "stateMutability": "view",
      "type": "function"
    },
    {
        // описание аргументов метода (кол-во выпускаемых ERC-721)
        "inputs": [
          {
            "internalType": "uint256",
            "name": "_quantity",
            "type": "uint256"
          }
        ],
        "name": "mint",
        "outputs": [],
        // payable означает что ожидает получить ETH
        "stateMutability": "payable",
        "type": "function"
      }
]
// кол-во выпускаемых токенов
const AMOUNT = 1

async function mint() {
      // получение цифровой подписи
      const signer = web3.getSigner()
      // инициализация контракта для чтения
      const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, provider)
      try {
        // получения стоимости выпуска токенов
        const price = await contract.getCost(AMOUNT)

        // данные для переопределения параметров транзакцийй
        const txOverrides = {
          value: price,
        }

        // получение комиссии, необходимой для подтверждения транзакции
        const gasPrice = await contract
          .estimateGas.mint(AMOUNT, txOverrides)

        // инициализация контракта для записи
        const contractWithSigner = await contract.connect(signer)

        // выпуск токена
        const tx = await contractWithSigner.mint(AMOUNT, {
          ...txOverrides,
          gasLimit: parseInt(gasPrice * 1.1)
        })

        // ожидание обработки транзакции
        const txReceipt = await txResponse.wait();
        console.log(`Transaction hash: ${txReceipt.hash}`);


      } catch (e) {
        console.log(e)
      }
    }
  }

Рассмотрим алгоритм кода выше:

Инициализируем данные необходимые для инициализации экземпляра смарт-контракта (CONTRACT_ADDRESS, ABI), а также аргумент необходимый для вызова метода (AMOUNT).

Инициализируем функцию mint.

Получаем цифровую подпись отправителя транзакции (signer) с помощью web3 провайдера ethers.

Инициализируем экземпляр contract для чтения данных.

Вызываем метод смарт-контракта getCost для получения стоимости выпуска токена. Аргументом передаем количество выпускаемых токенов. Данный метод не является стандартным для ERC-721 токенов и реализован непосредственно в самом смарт-контракте FrankenPunks для удобства.

Инициализируем данные для переопределения транзакции. В разделе "Cущность транзакции" мы разбирали из чего состоит неподтвержденная транзакция. Ethers позволяет не указывать параметры и использует заранее подготовленные значения и мы можем просто вызывать методы смарт-контракт, передавая туда лишь аргументы самого метода. Однако в нашем случае нам нужно переопределить количество ETH, которые будут отправлены смарт-контракту. Поэтому указываем в value, полученный price.

Обращаемся к свойству estimateGas экземпляра contract библиотеки ethers и имитируем выпуск токена, чтобы получить комиссию необходимую для подтверждения транзакции. Данное свойство доступно у всех экземпляров contract библиотеки ethers. Далее мы можем вызываем метод смарт-контракта для записи (mint). Аргументами передаем список параметров, которые будут переданы методу смарт-контракта. И последним опциональным аргументом передаем данные для переопределения транзакции.

Подключаемся к смарт-контракту с помощью метода connect, который доступен у всех экземпляров contract библиотеки ethers. С аргументом в виде цифровой подписи, которую ethers в дальнейшем сможет использовать для отправки транзакции. Данный метод возвращает новый экземпляр контракта для записи, который хранит в себе цифровую подпись отправителя.

Вызываем метод смарт-контракта mint, который выпустит токен и отправит его отправителю транзакции. Или другими словами запишет в блокчейн владельца токена. Вызов аналогичен описанному расчету комиссии, но также добавили повышенный лимит на комиссию gasLimit. Делать это было не обязательно, но благодаря большей комиссии транзакция обработается быстрее. Данный метод вернет нам экземпляр транзакции ethers.

У полученного экземпляра транзакции ethers вызываем метод wait. Который возвращает promise, ожидающий обработки транзакции. Из этого промиса после его разрешения можно также получить расширенный экземпляр транзакции с ее идентификатором (hash).

Обмен ERC-20 токенов

Всем известным примером смарт-контракта, который ожидает получить различные токены является смарт-контракт для обмена токенов.

ERC-20 также обозначает стандарт смарт-контракта с определенными методами.
В нашем случае нас интересует метод approve, который выдает разрешение смарт-контракту на использование токенов кошелька.


Существует множество смарт-контрактов, позволяющих производить обмен ERC-20. В данной статье мы рассмотрим обмен посредством смарт-контракта Uniswap. Мы также воспользуемся uniswap sdk, который содержит утилиты для более комфортного написания кода.

Uniswap - это один из ведущих протоколов торговли криптовалютой, доступный на основных Ethereum совместимых блокчейнах.

Для начала инициализируем данные, которые нам понадобятся:

const ERC_20_ABI = [
  {
        // описание аргументов метода (
        // адрес смарт-контракта которому выдается разрешение
        // кол-во токенов разрешенных для взымания
        // )
        "inputs": [
            {
                "internalType": "address",
                "name": "spender",
                "type": "address"
            },
            {
                "internalType": "uint256",
                "name": "value",
                "type": "uint256"
            }
        ],
        "name": "approve",
        "outputs": [
            {
                "internalType": "bool",
                "name": "",
                "type": "bool"
            }
        ],
        // nonpayable метод не взымает ETH
        "stateMutability": "nonpayable",
        "type": "function"
    },
]

const UNISWAP_ROUTER_ABI = [
  {
      // описание аргументов метода (
      // кол-во токенов на обмен
      // кол-во токенов взамен
      // [
      // адрес токена на обмен, 
      // адрес токена, получаемого взамен
      // ]
      // адрес получателя 
      // максимальное время ожидания
      // )
      "inputs":[
         {
            "internalType":"uint256",
            "name":"amountIn",
            "type":"uint256"
         },
         {
            "internalType":"uint256",
            "name":"amountOutMin",
            "type":"uint256"
         },
         {
            "internalType":"address[]",
            "name":"path",
            "type":"address[]"
         },
         {
            "internalType":"address",
            "name":"to",
            "type":"address"
         },
         {
            "internalType":"uint256",
            "name":"deadline",
            "type":"uint256"
         }
      ],
      "name":"swapExactTokensForTokens",
      "outputs":[
         {
            "internalType":"uint256[]",
            "name":"amounts",
            "type":"uint256[]"
         }
      ],
      // nonpayable метод не взымает ETH
      "stateMutability":"nonpayable",
      "type":"function"
   },
]

// адресс смарт-контракта uniswap для обменя
const UNISWAP_ROUTER_ADDRESS = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"

// адреса токенов для обмена
const USDT_CONTRACT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
const DAI_CONTRACT_ADDRESS = "0x6B175474E89094C44Da98b954EedeAC495271d0F"

Реализуем функцию approve для выдачи разрешения на использования токенов смарт-контракту Uniswap.

import { ethers } from 'ethers'

function approve() {
  // получение цифровой подписи
  const signer = web3.getSigner()
  // инициализация контракта токена USDT
  const tokenContract = new ethers.Contract(USDT_CONTRACT_ADDRESS, ERC_20_ABI, signer);
  
  try {
    // выдаем разрешение на использование токена USDT смарт-контракту Uniswap
    const tx = await tokenContract.approve(
      UNISWAP_ROUTER_ADDRESS,
      // указываем в разрешении макс допустимое значение,
      // чтобы не просить у отправителя разрешение каждый раз
      ethers.constants.MaxUint256
    );
    // ожидание обработки транзакции
    const txReceipt = await txResponse.wait();
    console.log(`Transaction hash: ${txReceipt.hash}`);


  } catch (e) {
    throw new Error(e)
  }
}

В данном случае для отправки транзакции нам не нужно дополнительно читать данные с смарт-контракта, поэтому экземпляр контракта инициализируется с аргументом signer вместо provider. Благодаря этому в момент вызова метода у нас автоматически происходит и подпись транзакции.

После того как разрешение успешно получено можно производить обмен. Для этого реализуем функцию swap, которая будет обменивать токен USDT на DAI в сети ethereum.

import { ethers } "ethers"
import {
  ChainId,
  Token,
  TokenAmount,
  Pair,
  Route,
  Trade,
  TradeType,
  Percent } from "@uniswap/sdk"

// токен для обменя
const TOKEN_IN = new Token(ChainId.MAINNET, USDT_CONTRACT_ADDRESS, 18)
// токен получаемые взамен
const TOKEN_OUT = new Token(ChainId.MAINNET, DAI_CONTRACT_ADDRESS, 18)

// кол-во токена для обменя
const AMOUNT_IN = ethers.utils.parseUnits('1', 18)
// преобразование кол-ва в вид необходимый смарт-контракту
const TOKEN_AMOUNT_IN = new TokenAmount(TOKEN_IN, AMOUNT_IN.toString())

function swap () {
  // получение цифровой подписи
  const signer = web3.getSigner()

  // инициализация контракта для записи
  const contractWithSigner = new ethers.Contract(
    UNISWAP_ROUTER_ADDRESS,
    UNISWAP_ROUTER_ABI,
    signer
  )

  // получения данных пары необходимых для создания маршрута обмена
  const pair = await Pair.fetchData(TOKEN_AMOUNT_IN.token, TOKEN_OUT, provider)
  // инициализация маршрута обмена
  const route = new Route([pair], TOKEN_AMOUNT_IN.token)
  // инициализация класса для расчета кол-ва получаемого взамен токена
  const trade = new Trade(route, TOKEN_AMOUNT_IN, TradeType.EXACT_INPUT)
  // инициализация процента проскальзывания
  // (насколько кол-во получаемого токена может отличаться)
  const slippageTolerance = new Percent('1', '100') // 1%
  // расчет кол-ва получаемого токена
  const amountOutMin = trade.minimumAmountOut(slippageTolerance).raw.toString()
  // инициализация максимального времени ожидания обмена
  const deadline = Math.floor(Date.now() / 1000) + 60 * 10 // 10 minute
  try {
    // вызов метода для обмена токенов, инициализация транзакции
    const txReceipt = await contractWithSigner.swapExactTokensForTokens(
      amountIn.toString(),
      amountOutMin,
      [tokenIn.address, tokenOut.address],
      signer.getAddress(),
      deadline,
    )

    // ожидание обработки транзакции
    const txReceipt = await txResponse.wait()

    console.log(`Transaction hash: ${txReceipt.hash}`)

    } catch (e) {
        throw new Error(e)
    }
    
  }

В данном примере пристутствуют вычислительные операции, которые необходимы смарт-контракту uniswap для построения маршрута обмена. Самый простой маршрут может быть USDT-DAI. Сложный может включать промежуточные обмены, если прямое предложение не найдено. Например: USDT-USDC-BUSD-DAI.

В остальном алгоритм отправки транзакции такой же, как и у метода approve у ERC-20 токенов и схож с методом mint у ERC-721 токенов.

Бонус: Отслеживание ускорения и отмены транзакции

На этом моменте, хотелось бы выразить свое уважение и порадовать, что у вас есть все знания, чтобы отправлять и отслеживать транзакции в javascript приложения. Думаю эта бонусная тема тоже будет для вас интересна, так как в документации ее я не нашел. Но на практике она часто ставит отправителя в ступор.

Дело в том что после отправки транзакции мы получаем объект содержащий hash транзакции, который нужен, чтобы искать ее в explorer. В качестве своего рода чековой квитанции. Поэтому достаточно часто в децентрализованных приложениях есть отдельный раздел с историей транзакций, который содержит ссылки для быстрой навигации в explorer:

const link = `https://etherscan.io/tx/${transactionHash}`

Однако иногда возникает ситуация, когда отправитель ускоряет или вовсе отменяет транзакцию. В таком случае метод wait выкинет ошибку. Если в случае отмены еще все понятно, то в случае ускорения транзакция все равно будет выполнена. Но дело в том, что ускорение отменяет текущую транзакцию и создает новую, с увеличенной комиссией за обработку. Следовательно меняется и ее идентификатор (hash).

Давай реализуем функцию handleTxErrors, которая будет обрабатывать эти случаи:

function handleTxErrors (err, tx) {
  const newTx = { ...tx, ...err.replacement }
  switch (err.reason) {
    case 'cancelled':
      if (err.cancelled) {
        onsole.log(`Transaction cancelled: ${newTx.hash}`)
      }
    break
    case 'repriced':
      if (!err.cancelled) {
        console.log(`Transaction repriced: ${newTx.hash}`)
      break
    default:
      throw new Error(err)
    }
}

Функция принимает в качестве аргументов ошибку и старую транзакцию, обрабатывает причину ошибки и устанавливает новый идентификатор транзакции (hash). Таким образом, подставив этот обработчик в блок catch можно обеспечить еще более приятный пользовательский опыт.

Вывод

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

Комментарии (0)