Как взаимодействовать со смарт-контрактами с помощью библиотеки web3 на Python

Содержание:

Этот пост для тех, кто впервые сталкивается с задачей автоматизации EVM смарт-контрактов с помощью библиотеки web3 на Python.

В этой презентации пытаюсь разобраться сам и поделиться тем, как взаимодействовать со смарт-контрактами через web3.py. Здесь не будет сценариев, функций и классов, чтобы сделать код лаконичнее и короче (DRY), просто код-пример взаимодействия со смарт-контрактами.

Цель статьи: показать, какие могут быть способы поиска информации, которая поможет понять смарт-контракт и сделать автоматизацию на web3.py.

Готовые материалы и ресурсы на эту тему

Для начала хочу порекомендовать ресурсы, где вы можете найти готовые материалы на эту тему.
  • Статья на habr от Никиты Шамаева
Статьи написаны в 2022 году, на данный момент синтаксис функций изменился, но эти материалы закрывают почти все вопросы по автоматизации web3.py:
  • отправка обычных транзакций
  • взаимодействие со смарт-контрактами (ERC-20, NFT)
  • оценка газа
  • multicall
  • Программист Ahillary - записывает видео о web3 автоматизации на Youtube, обучает автоматизации на авторских курсах:
Ahillary подготовил мощнейшую статью-гайд, которая поможет вам начать автоматизировать взаимодействие со смарт-контрактами. Обязательно нужно прочитать, внутри статьи много ссылок на другие полезные статьи. Про автоматизацию смарт-контрактов читайте в разделе "Чем занимаются web3 программисты?" и "Практическая часть: разбор WooFi свапа".
  • Holdmod - топ крипто-абузеры, которые делятся с сообществом готовым софтом и делают свои dapp:

Базовые понятия

Чтобы ознакомиться со смарт-контрактом и сделать автоматизацию, вам нужно:
  • адрес смарт-контракта, например 0xE592427A0AEce92De3Edee1F18E0157C05861564 - Uniswap Router V3
  • Read или Write функции, которые вы найдете в сканере во вкладке Contract:

С помощью Read-функций вы можете получить какую-то информацию из смарт-контракта, например данные о позиции в пуле ликвидности (рассмотрим пример ниже).

Write функции вносят изменения в блокчейн и потребуют оплаты комиссий, проще говоря это транзакции в dapps, например обмен одной монеты на другую.

  • ABI - интерфейс для общения со смарт-контрактом. Чтобы воспользоваться функцией смарт-контракта, она должна быть описана в ABI. Статья на эту тему. ABI вы сможете найти в сканере во вкладке Contract в разделе Code:
ABI скопируется в совершенно нечитаемом виде:
Используйте любой json конвертер, чтобы привести ABI в удобный вид, например https://jsonformatter.org/, а потом вставляйте в редактор кода:
Для удобства в своих программах используйте отдельные json файлы, чтобы не вставлять весь ABI в свой код:
import json
with open('abi.json', 'r') as file:
    abi_json = json.load(file)

Как находить информацию, которая поможет автоматизировать смарт-контракт

1. Сделать транзакцию в dapp руками и посмотреть контракт в сканере

Самый адекватный вариант, с которого стоит начать. Совершаете транзакцию в dapp и переходите в сканер. Например, через Metamask. Нажмите на адрес контракта, затем перейдите в сканер:
Вы перейдете на страницу сканера с информацией о контракте. Во вкладке Contract во Write Contract найдите название функции, которое было показано через Metamask, в примере это Execute, разверните, вы увидите, какие аргументы необходимо передать:
В разделе Code вы найдете ABI:

2. Искать документацию разработчиков

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

Например, документация Uniswap https://docs.uniswap.org/contracts/v3/overview

3. Искать готовый код на Github

Поиск по GitHub может очень ускорить разработку вашей автоматизации, потому что кто-то уже решил эту задачу и поделился кодом:

4. Искать в поисковике

И конечно же стоит как можно больше обращаться к поисковым системам. Ищите опубликованные посты с кодом или ответы на StackOverflow. Пробуйте искать не только в Google, но и в Yandex - он лучше понимает смысл запроса. Также используйте операторы поиска, чтобы конкретизировать критерии для поиска. Например, запрос "github intext:merkly gas refuel". Оператор intext будет указывать поисковику, что нужно искать перечисленные ключевые слова в тексте страниц:
Пример операторов поиска в статье https://seranking.com/ru/blog/operatory-poiska-google/

Как отправлять обычные транзакции с помощью Python

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

Подробное описание видов транзакций https://docs.infura.io/networks/ethereum/concepts/transaction-types

1. Legacy

Этот вид транзакций использовался до форка London, в нем необходимо указывать параметры gasPrice и gas:
Импорт библиотеки:
from web3 import Web3
Подключение:
web3 = Web3(Web3.HTTPProvider('https://arbitrum-one.public.blastapi.io'))

Настройки кошелька

Приватник:

PRIVATE_KEY = 'ggd6e4cb6770f7c1f8e7b211db20341a673fb23e885955dbn4f6hhhjkl675f1e'
Публичный адрес из приватника:
PUBLIC_ADDRESS = web3.eth.account.from_key(PRIVATE_KEY).address
Сколько ETH будет отправлено:
AMOUNT = 0.0001

Настройки транзакции

Цена газа:

gas_price = web3.eth.gas_price
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'to': PUBLIC_ADDRESS,
        'gasPrice': gas_price,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS),
        'value': web3.to_wei(AMOUNT, 'ether')
}
Расчет газа для транзакции и добавление в настройки:
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit)})
Подпись:
signed_transaction = web3.eth.account.sign_transaction(transaction_settings, PRIVATE_KEY)
Отправка:
transaction_hash = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)

2. EIP - 1559

Вид транзакций, в котором необходимо указывать параметры gasPrice, maxPriorityFeePerGas, maxFeePerGas.

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

Также прочитайте статью, которую Ahillary упоминал в видео https://habr.com/ru/amp/publications/655615/

Отрывки из статьи:

  • "В новой системе расчета стоимость газа складывается из base fee - базовая комиссия, которая будет сожжена, и комиссии за включение блока (inclusion fee)."
  • "maxFeePerGas - максимальная стоимость, которую вы готовы заплатить за газ, состоит из baseFee + priorityFee maxPriorityFeePerGas - составляющая priorityFee"
  • "Для управления скоростью можно использовать различные комбинации параметров, управляющих комиссией для майнеров. Значение для maxPriorityFeePerGas получаем из eth_maxPriorityFeePerGas. Далее рассчитываемmaxFeePerGas = maxPriorityFeePerGas + baseFee * 2. Значение для baseFee берем из последнего блока. Умножение значения на 2 позволяет не учитывать возможные увеличения комиссии в зависимости от заполненности блоков, а сразу заложить максимальное значение. Такой подход позволит включить транзакцию в блок, и вероятность будет зависеть только от попадания maxPriorityFeePerGas в ожидания майнеров. То есть комбинацией maxFeePerGas и maxPriorityFeePerGas можно наложить ограничение на включение в блок в зависимости от роста/падения baseFee."
Импорт библиотеки:
from web3 import Web3
Подключение:
web3 = Web3(Web3.HTTPProvider('https://arbitrum-one.public.blastapi.io'))

Настройки кошелька

Приватник:

PRIVATE_KEY = 'ggd6e4cb6770f7c1f8e7b211db20341a673fb23e885955dbn4f6hhhjkl675f1e'
Публичный адрес из приватника:
PUBLIC_ADDRESS = web3.eth.account.from_key(PRIVATE_KEY).address
Сколько ETH будет отправлено:
AMOUNT = 0.0001

Настройки транзакции

Данные о последнем блоке:

last_block = web3.eth.get_block('latest')
Расчет для параметра maxFeePerGas:
max_priority_fee_per_gas = web3.eth.max_priority_fee
base_fee = int(last_block['baseFeePerGas'] * 2)
max_fee_per_gas = base_fee + max_priority_fee_per_gas
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'to': PUBLIC_ADDRESS,
        'maxPriorityFeePerGas': max_priority_fee_per_gas,
        'maxFeePerGas': max_fee_per_gas,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS),
        'value': web3.to_wei(AMOUNT, 'ether')
        }
Расчет газа для транзакции и добавление в настройки:
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit)})
Подпись:
signed_transaction = web3.eth.account.sign_transaction(transaction_settings, PRIVATE_KEY)
Отправка:
created_tx = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)

Как подключиться к смарт-контракту через Python

Импорт библиотеки:
from web3 import Web3
Подключение:
web3 = Web3(Web3.HTTPProvider('https://arbitrum-one.public.blastapi.io'))
Адрес контракта:
CONTRACT_ADDRESS = '0xE592427A0AEce92De3Edee1F18E0157C05861564'
ABI контракта. Вставляю полностью ABI в одну строчку, так лучше не делать в ваших программах. Выше я писал о том, что удобнее формировать отдельные json-файлы и загружать ABI из них, но для презентации сгодится:)
CONTRACT_ABI = '[{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH9","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH9","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"}],"internalType":"struct ISwapRouter.ExactInputParams","name":"params","type":"tuple"}],"name":"exactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct ISwapRouter.ExactInputSingleParams","name":"params","type":"tuple"}],"name":"exactInputSingle","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"}],"internalType":"struct ISwapRouter.ExactOutputParams","name":"params","type":"tuple"}],"name":"exactOutput","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct ISwapRouter.ExactOutputSingleParams","name":"params","type":"tuple"}],"name":"exactOutputSingle","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"refundETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowed","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowedIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"sweepToken","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"sweepTokenWithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"uniswapV3SwapCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"unwrapWETH9","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"unwrapWETH9WithFee","outputs":[],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}]'
Подключение:
contract = web3.eth.contract(address=CONTRACT_ADDRESS, abi=CONTRACT_ABI)

Функции этого контракта, которые есть в сканере также можно посмотреть через Python.

Список доступных методов объекта:

dir(contract)
Среди методов есть all_functions - просмотр solidity функций:
contract.all_functions()

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

По-другому можно посмотреть:

dir(contract.functions)
Найти нужную функцию и вызвать ее:
В исключении может быть написано какие аргументы должны быть переданы.

Использование укороченного (кастомного) ABI

Выше был использован полный ABI смарт-контракта, который был скопирован из сканера - в нем перечислены все функции. Можно оставить только нужные функции.

Пример abi только с одной функцией exactInputSingle:

CONTRACT_ABI = '[{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct ISwapRouter.ExactInputSingleParams","name":"params","type":"tuple"}],"name":"exactInputSingle","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"}]'
Подключение к тому же контракту:
contract = web3.eth.contract(address=CONTRACT_ADDRESS, abi=CONTRACT_ABI)
Просмотр доступных функций:
contract.all_functions()

Примеры взаимодействия с контрактами

Часть 1 - ERC-20

Токены ERC-20 это тоже смарт-контракты. Посмотрим пару примеров.

Базовые настройки

Задам переменные, которые будут использоваться в примерах с ERC-20.
Подключение к Arbitrum:
from web3 import Web3
web3 = Web3(Web3.HTTPProvider('https://arbitrum-one.public.blastapi.io'))

Настройки кошелька

Приватник:

PRIVATE_KEY = 'ggd6e4cb6770f7c1f8e7b211db20341a673fb23e885955dbn4f6hhhjkl675f1e'
Публичный адрес из приватника:
PUBLIC_ADDRESS = web3.eth.account.from_key(PRIVATE_KEY).address

Пример 1 - USDC

Для первого примера стейблкоин USDC. Сделаю вызов просмотра баланса и выдам апрув на использование токена смарт-контракту.

Адрес контракта из Arbiscan:

USDC_CONTRACT_ADDRESS = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'
Функции контракта, ищу balanceOf среди Read-функций и approve среди Write-функций:

Видно, что нужных функций нет.

Просмотр разделов Read as Proxy, Write as Proxy:

В этих разделах есть нужные функции. Обратите внимание на сообщение:"ABI for the implementation contract at 0x0f4fb9474303d10905ab86aa8d5a65fe44b6e04a, using OpenZeppelin's Unstructured Storage proxy pattern.". Нужно перейти на страницу указанного контракта, там будут нужные функции в коде и в ABI.

Копирую ABI со страницы указанного контракта:

USDC_CONTRACT_ABI = '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"authorizer","type":"address"},{"indexed":true,"internalType":"bytes32","name":"nonce","type":"bytes32"}],"name":"AuthorizationCanceled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"authorizer","type":"address"},{"indexed":true,"internalType":"bytes32","name":"nonce","type":"bytes32"}],"name":"AuthorizationUsed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_account","type":"address"}],"name":"Blacklisted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"newBlacklister","type":"address"}],"name":"BlacklisterChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"burner","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Burn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"newMasterMinter","type":"address"}],"name":"MasterMinterChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"minter","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Mint","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"minter","type":"address"},{"indexed":false,"internalType":"uint256","name":"minterAllowedAmount","type":"uint256"}],"name":"MinterConfigured","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldMinter","type":"address"}],"name":"MinterRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":false,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[],"name":"Pause","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"newAddress","type":"address"}],"name":"PauserChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"newRescuer","type":"address"}],"name":"RescuerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_account","type":"address"}],"name":"UnBlacklisted","type":"event"},{"anonymous":false,"inputs":[],"name":"Unpause","type":"event"},{"inputs":[],"name":"CANCEL_AUTHORIZATION_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PERMIT_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RECEIVE_WITH_AUTHORIZATION_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TRANSFER_WITH_AUTHORIZATION_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"authorizer","type":"address"},{"internalType":"bytes32","name":"nonce","type":"bytes32"}],"name":"authorizationState","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"}],"name":"blacklist","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"blacklister","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"burn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"authorizer","type":"address"},{"internalType":"bytes32","name":"nonce","type":"bytes32"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"cancelAuthorization","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"minter","type":"address"},{"internalType":"uint256","name":"minterAllowedAmount","type":"uint256"}],"name":"configureMinter","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"currency","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"decrement","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"increment","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"tokenName","type":"string"},{"internalType":"string","name":"tokenSymbol","type":"string"},{"internalType":"string","name":"tokenCurrency","type":"string"},{"internalType":"uint8","name":"tokenDecimals","type":"uint8"},{"internalType":"address","name":"newMasterMinter","type":"address"},{"internalType":"address","name":"newPauser","type":"address"},{"internalType":"address","name":"newBlacklister","type":"address"},{"internalType":"address","name":"newOwner","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"newName","type":"string"}],"name":"initializeV2","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"lostAndFound","type":"address"}],"name":"initializeV2_1","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"}],"name":"isBlacklisted","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"isMinter","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"masterMinter","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"mint","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"minter","type":"address"}],"name":"minterAllowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pauser","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"permit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"validAfter","type":"uint256"},{"internalType":"uint256","name":"validBefore","type":"uint256"},{"internalType":"bytes32","name":"nonce","type":"bytes32"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"receiveWithAuthorization","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"minter","type":"address"}],"name":"removeMinter","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IERC20","name":"tokenContract","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"rescueERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"rescuer","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"validAfter","type":"uint256"},{"internalType":"uint256","name":"validBefore","type":"uint256"},{"internalType":"bytes32","name":"nonce","type":"bytes32"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"transferWithAuthorization","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"}],"name":"unBlacklist","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newBlacklister","type":"address"}],"name":"updateBlacklister","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newMasterMinter","type":"address"}],"name":"updateMasterMinter","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newPauser","type":"address"}],"name":"updatePauser","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newRescuer","type":"address"}],"name":"updateRescuer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"version","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}]'
Подключение:
usdc_contract = web3.eth.contract(address=USDC_CONTRACT_ADDRESS, abi=USDC_CONTRACT_ABI)
Просмотр функций:

Просмотр баланса

Передаю публичный адрес кошелька в read-функцию и вызываю методом call:
balance = usdc_contract.functions.balanceOf(PUBLIC_ADDRESS).call()
Не похоже, что у меня есть 65 миллионов долларов:) У каждого токена есть свое количество знаков после запятой. Об этом написано в первой статье Никиты Шамаева на habr (ссылка в начале моей статьи).
decimals = usdc_contract.functions.decimals().call()
real_balance = int(balance/ 10 ** decimals)
На балансе 65 usdc.

Выдача approve смарт-контракту

Чтобы токен мог использоваться функциями смарт-контракта, нужно выдать разрешение на нужное количество токенов. Read-функция approve принимает адрес смарт-контракта и количество токенов:
Адрес контракта, которому надо выдать разрешение:
CONTRACT_ADDRESS = '0xE592427A0AEce92De3Edee1F18E0157C05861564'
Настройка транзакции EIP-1559:
last_block = web3.eth.get_block('latest')
max_priority_fee_per_gas = web3.eth.max_priority_fee
base_fee = int(last_block['baseFeePerGas'] * 2)
max_fee_per_gas = base_fee + max_priority_fee_per_gas
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'maxPriorityFeePerGas': max_priority_fee_per_gas,
        'maxFeePerGas': max_fee_per_gas,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS),
        }
Расчет газа для транзакции и добавление в настройки:
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit) * 2})
Инициализация:
approve_tx = usdc_contract.functions.approve(CONTRACT_ADDRESS, 
                                             real_balance).build_transaction(transaction_settings)   
Подпись:
signed_transaction = web3.eth.account.sign_transaction(approve_tx, PRIVATE_KEY)
Отправка:
created_tx = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)

Просмотр количества токенов, которые может использовать смарт-контракт

Read-функция allowance принимает ваш публичный адрес и адрес контракта:
usdc_contract.functions.allowance(PUBLIC_ADDRESS, CONTRACT_ADDRESS).call()

Пример 2 - WETH

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

Для оборачивания использую 0.001 ETH:

AMOUNT = 0.001
Контракт:
WETH_CONTRACT_ADDRESS = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1'
Мне нужна функция deposit из раздела Write as Proxy:

Как в примере с USDC, в разделе Write as Proxy указан другой контракт - 0x8b194beae1d3e0788a1a35173978001acdfba668.

Мне нужно взять ABI со страницы этого контракта:

WETH_CONTRACT_ABI = '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"bridgeBurn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"bridgeMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"deposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"depositTo","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"},{"internalType":"uint8","name":"_decimals","type":"uint8"},{"internalType":"address","name":"_l2Gateway","type":"address"},{"internalType":"address","name":"_l1Address","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"l1Address","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"l2Gateway","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"permit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"transferAndCall","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]'
Подключение:
weth_contract = web3.eth.contract(address=WETH_CONTRACT_ADDRESS, abi=WETH_CONTRACT_ABI)

Оборачивание

Настройка транзакции:
last_block = web3.eth.get_block('latest')
max_priority_fee_per_gas = web3.eth.max_priority_fee
base_fee = int(last_block['baseFeePerGas'] * 2)
max_fee_per_gas = base_fee + max_priority_fee_per_gas
Сама функция deposit не принимает никаких аргументов, передаю оборачиваемое количество в параметре value:
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'maxPriorityFeePerGas': max_priority_fee_per_gas,
        'maxFeePerGas': max_fee_per_gas,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS),
        'value': web3.to_wei(AMOUNT, 'ether')
        }
Расчет газа для транзакции и добавление в настройки:
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit) * 2})
Инициализация:
wrap_tx = weth_contract.functions.deposit().build_transaction(transaction_settings)   
Подпись:
signed_transaction = web3.eth.account.sign_transaction(wrap_tx, PRIVATE_KEY)
Отправка:
created_tx = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)
Теперь ETH может использоваться в смарт-контрактах, но чтобы он использовался нужно выдать approve, точно так же как в примере с USDC.

Разворачивание WETH обратно в ETH

Для этого используется функция withdraw.

Настройка транзакции:

last_block = web3.eth.get_block('latest')
max_priority_fee_per_gas = web3.eth.max_priority_fee
base_fee = int(last_block['baseFeePerGas'] * 2)
max_fee_per_gas = base_fee + max_priority_fee_per_gas
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'maxPriorityFeePerGas': max_priority_fee_per_gas,
        'maxFeePerGas': max_fee_per_gas,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS)
        }
Расчет газа для транзакции и добавление в настройки:
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit) * 2})
Функция withdraw принимает в качестве аргумента разворачиваемое количество WETH:
unwrap_tx = weth_contract.functions.withdraw(web3.to_wei(AMOUNT, 'ether')).build_transaction(transaction_settings)   
Подпись:
signed_transaction = web3.eth.account.sign_transaction(unwrap_tx, PRIVATE_KEY)
Отправка:
created_tx = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)
WETH был отправлен на адрес для сожжения, и смарт контракт разблокировал и вернул ETH на мой адрес.

Часть 2 - взаимодействие с DEFI через Python

В примерах сделаю:
  • обмен USDC на WETH на Uniswap
  • добавлю ликвидность на Uniswap, а затем выведу средства

Базовые настройки

Задам переменные, которые будут использоваться во всех примерах.

Библиотеки

from web3 import Web3
import datetime
from datetime import timedelta
import requests
import json

Подключение

В примере использую RPC URL для Arbitrum:
web3 = Web3(Web3.HTTPProvider('https://arbitrum-one.public.blastapi.io'))

Настройки кошелька

Приватник:
PRIVATE_KEY = 'ggd6e4cb6770f7c1f8e7b211db20341a673fb23e885955dbn4f6hhhjkl675f1e'
Публичный адрес из приватника:
PUBLIC_ADDRESS = web3.eth.account.from_key(PRIVATE_KEY).address

Настройки смарт контрактов токенов

WETH
WETH_CONTRACT_ADDRESS = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1'
WETH_CONTRACT_ABI = '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"bridgeBurn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"bridgeMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"deposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"depositTo","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_name","type":"string"},{"internalType":"string","name":"_symbol","type":"string"},{"internalType":"uint8","name":"_decimals","type":"uint8"},{"internalType":"address","name":"_l2Gateway","type":"address"},{"internalType":"address","name":"_l1Address","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"l1Address","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"l2Gateway","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"permit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"transferAndCall","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdrawTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]'
USDC
USDC_CONTRACT_ADDRESS = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'
USDC_CONTRACT_ABI = '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"authorizer","type":"address"},{"indexed":true,"internalType":"bytes32","name":"nonce","type":"bytes32"}],"name":"AuthorizationCanceled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"authorizer","type":"address"},{"indexed":true,"internalType":"bytes32","name":"nonce","type":"bytes32"}],"name":"AuthorizationUsed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_account","type":"address"}],"name":"Blacklisted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"newBlacklister","type":"address"}],"name":"BlacklisterChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"burner","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Burn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"newMasterMinter","type":"address"}],"name":"MasterMinterChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"minter","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Mint","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"minter","type":"address"},{"indexed":false,"internalType":"uint256","name":"minterAllowedAmount","type":"uint256"}],"name":"MinterConfigured","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldMinter","type":"address"}],"name":"MinterRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":false,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[],"name":"Pause","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"newAddress","type":"address"}],"name":"PauserChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"newRescuer","type":"address"}],"name":"RescuerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_account","type":"address"}],"name":"UnBlacklisted","type":"event"},{"anonymous":false,"inputs":[],"name":"Unpause","type":"event"},{"inputs":[],"name":"CANCEL_AUTHORIZATION_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PERMIT_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"RECEIVE_WITH_AUTHORIZATION_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"TRANSFER_WITH_AUTHORIZATION_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"authorizer","type":"address"},{"internalType":"bytes32","name":"nonce","type":"bytes32"}],"name":"authorizationState","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"}],"name":"blacklist","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"blacklister","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"burn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"authorizer","type":"address"},{"internalType":"bytes32","name":"nonce","type":"bytes32"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"cancelAuthorization","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"minter","type":"address"},{"internalType":"uint256","name":"minterAllowedAmount","type":"uint256"}],"name":"configureMinter","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"currency","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"decrement","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"increment","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"tokenName","type":"string"},{"internalType":"string","name":"tokenSymbol","type":"string"},{"internalType":"string","name":"tokenCurrency","type":"string"},{"internalType":"uint8","name":"tokenDecimals","type":"uint8"},{"internalType":"address","name":"newMasterMinter","type":"address"},{"internalType":"address","name":"newPauser","type":"address"},{"internalType":"address","name":"newBlacklister","type":"address"},{"internalType":"address","name":"newOwner","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"newName","type":"string"}],"name":"initializeV2","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"lostAndFound","type":"address"}],"name":"initializeV2_1","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"}],"name":"isBlacklisted","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"isMinter","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"masterMinter","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"mint","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"minter","type":"address"}],"name":"minterAllowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pauser","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"permit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"validAfter","type":"uint256"},{"internalType":"uint256","name":"validBefore","type":"uint256"},{"internalType":"bytes32","name":"nonce","type":"bytes32"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"receiveWithAuthorization","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"minter","type":"address"}],"name":"removeMinter","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"contract IERC20","name":"tokenContract","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"rescueERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"rescuer","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"validAfter","type":"uint256"},{"internalType":"uint256","name":"validBefore","type":"uint256"},{"internalType":"bytes32","name":"nonce","type":"bytes32"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"transferWithAuthorization","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_account","type":"address"}],"name":"unBlacklist","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpause","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newBlacklister","type":"address"}],"name":"updateBlacklister","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newMasterMinter","type":"address"}],"name":"updateMasterMinter","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newPauser","type":"address"}],"name":"updatePauser","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newRescuer","type":"address"}],"name":"updateRescuer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"version","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}]'

Пример 1 - Swap на Uniswap

Чтобы понять, какой смарт-контракт и каким образом осуществляет обмен на Uniswap, попробую самый очевидный способ - взаимодействие через Metamask:
Перехожу в сканер и ищу нужную функцию:

Функция принимает аргумент commands в виде байт-кода, еще какой-то параметр inputs список с байт-кодом, deadline в виде целого числа.

Из этого набора мне ничего непонятно.

Обращусь к документации.

Документация Uniswap - https://docs.uniswap.org/

Single Swaps - https://docs.uniswap.org/contracts/v3/guides/swaps/single-swaps

К ним относятся функции exactInputSingle, exactOutputSingle:

  • exactInputSingle - обычный swap, именно такой будет в примере, обмен 15 USDC на максимально возможное по курсу ETH.
  • exactOutputSingle - обратная логика, здесь мы бы покупали фиксированное количество ETH, например 0.1, за USDC по актуальному курсу.

Multihop Swaps - https://docs.uniswap.org/contracts/v3/guides/swaps/multihop-swaps

К ним относятся функции exactInput, exactOutput:

  • exactInput - Покупка через промежуточные токены, например обменять установленное количество ETH на максимально возможное количество по курсу USDC, а потом на максимально возможное по курсу ARB в одной транзакции. Но можно просто поменять ETH на UCDC таким методом.
  • exactOutput - аналогично с exactOutputSingle устанавливаете желаемое количество конечного токена, например ARB, который будет куплен за USDC из ETH по актуальному курсу.

Теперь нужно найти адрес контракта, который выполняет эти функции.

Список адресов контрактов Uniswap - https://docs.uniswap.org/contracts/v3/reference/deployments

Перехожу на страницу контракта в сканере и нахожу нужную функцию для свапа exactInputSingle:
Функция принимает аргумент params. Пробую найти в документации описание этой функции:
На странице с документацией есть описания параметров:
  • tokenIn - адрес токена, который меняем (меняем USDC),
  • tokenOut - адрес токена, на которой меняем (меняем на ETH),
  • fee - комиссия пула, какой процент от меняемой суммы будет заплачено в пул, например 3000 - это 0,3%, или 10000 - 1%,
  • recipient - адрес получателя, т.е. наш публичный адрес,
  • deadline - время в формате Unix после которого обмен не состоится, защиты от сэндвич атак, сейчас устанавливается текущее время + 20 секунд,
  • amountIn - сколько USDC будет поменяно,
  • amountOutMinimum - на сколько я понимаю минимальное количество токенов, которое будет в результате обмена, тоже защита от атак, можно оставить 0 как в документации
  • sqrtPriceLimitX96 - какой-то еще параметр для защиты от влияния на цену:)

Теперь непосредственно обмен на Uniswap через Python

Сколько USDC будет обменяно:
AMOUNT = 15
Адрес контракта из Arbiscan:
UNISWAP_CONTRACT_ADDRESS = '0xE592427A0AEce92De3Edee1F18E0157C05861564'
ABI контракта из Arbiscan:
UNISWAP_CONTRACT_ABI = '[{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH9","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH9","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"}],"internalType":"struct ISwapRouter.ExactInputParams","name":"params","type":"tuple"}],"name":"exactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct ISwapRouter.ExactInputSingleParams","name":"params","type":"tuple"}],"name":"exactInputSingle","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"}],"internalType":"struct ISwapRouter.ExactOutputParams","name":"params","type":"tuple"}],"name":"exactOutput","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct ISwapRouter.ExactOutputSingleParams","name":"params","type":"tuple"}],"name":"exactOutputSingle","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"refundETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowed","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowedIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"sweepToken","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"sweepTokenWithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"uniswapV3SwapCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"unwrapWETH9","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"unwrapWETH9WithFee","outputs":[],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}]'

Инициализация смарт-контрактов

Для обмена мне нужно использовать контракт Uniswap, контракт USDC, чтобы взять значение decimals и выдать approve.
uniswap_contract = web3.eth.contract(address=UNISWAP_CONTRACT_ADDRESS, abi=UNISWAP_CONTRACT_ABI)
usdc_contract = web3.eth.contract(address=USDC_CONTRACT_ADDRESS, abi=USDC_CONTRACT_ABI)

USDC - decimals, approve

decimals
usdc_decimals = usdc_contract.functions.decimals().call()
usdc_for_swap = AMOUNT * 10 ** usdc_decimals
approve
Настройка транзакции:
last_block = web3.eth.get_block('latest')
max_priority_fee_per_gas = web3.eth.max_priority_fee
base_fee = int(last_block['baseFeePerGas'] * 2)
max_fee_per_gas = base_fee + max_priority_fee_per_gas
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'maxPriorityFeePerGas': max_priority_fee_per_gas,
        'maxFeePerGas': max_fee_per_gas,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS)
        }
Расчет газа для транзакции и добавление в настройки:
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit) * 2})
Разрешение контракту в переменной UNISWAP_CONTRACT_ADDRESS на использование количества токенов, которые указаны в usdc_for_swap:
approve_tx = usdc_contract.functions.approve(UNISWAP_CONTRACT_ADDRESS, 
                                             usdc_for_swap).build_transaction(transaction_settings) 
Подпись:
signed_transaction = web3.eth.account.sign_transaction(approve_tx, PRIVATE_KEY)
Отправка:
created_tx = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)

Uniswap - exactInputSingle

Настройка параметров, которые будут переданы в функцию. Deadline формируется из текущего времени + 60 секунд:
uniswap_params = {
    'tokenIn': USDC_CONTRACT_ADDRESS,
    'tokenOut': WETH_CONTRACT_ADDRESS,
    'fee'3000,
    'recipient': PUBLIC_ADDRESS,
    'deadline'int((datetime.datetime.now() + timedelta(seconds=60)).timestamp()),
    'amountIn': usdc_for_swap,
    'amountOutMinimum'0,
    'sqrtPriceLimitX96'0,
    }
Параметры транзакции:
last_block = web3.eth.get_block('latest')
max_priority_fee_per_gas = web3.eth.max_priority_fee
base_fee = int(last_block['baseFeePerGas'] * 2)
max_fee_per_gas = base_fee + max_priority_fee_per_gas
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'maxPriorityFeePerGas': max_priority_fee_per_gas,
        'maxFeePerGas': max_fee_per_gas,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS)
        }
Расчет газа для транзакции и добавление в настройки:
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit) * 2})
Запуск функции:
uniswap_single_swap = (
                        uniswap_contract.functions.exactInputSingle(uniswap_params)
                        .build_transaction(transaction_settings)  
                      )
Подпись:
signed_transaction = web3.eth.account.sign_transaction(uniswap_single_swap, PRIVATE_KEY)
Отправка:
created_tx = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)

Uniswap - Multihop Swap, функция exactInput

Прочитайте разбор этой функции в статье от crjameson.

Пример 2 - Добавление ликвидности в пул на Uniswap

Также как при выполнении обмена, чтобы понять, какой смарт-контракт и каким образом осуществляется добавление ликвидности взаимодействую с интерфейсом Uniswap через Metamask:
Функция mint находится по указанному адресу, она принимает кортеж params:
Обратите внимание, что в примере я использую добавление ликвидности в паре WETH/USDC, и контракт использует функцию mint. Если добавлять пару ETH/USDC через интерфейс Uniswap, будет функция multicall, в рамках которой ETH будет обернут в WETH, и выполнится функция mint. Multicall делает несколько действий в рамках одной транзакции и экономит время и деньги, подробно о multicall написано в статье Никиты Шамаева на habr (ссылка в начале моей статьи). Multicall я сделаю в следующем примере, где нужно будет забрать ликвидность из пула.
Описание функции в документации - https://docs.uniswap.org/contracts/v3/guides/providing-liquidity/mint-a-position
Какие у нас тут параметры:
  • token0 - адрес контракта первого токена в паре - WETH,
  • token1 - адрес второго - USDC,
  • fee - комиссия пула, такой же параметр как в примере с обменом - 3000 = 0,3% и т.п.,

два параметра которые указывают диапазон, в котором участвует добавляемая ликвидность:
  • tickLower: -887220,
  • tickUpper: 887220,
такие параметры означают Full Range

  • amount0Desired - какое количество первого токена хотим добавить,
  • amount1Desired - какое количество второго токена,

следующие два параметра вроде бы служат защитой от проскальзывания, можно оставить по нулям:)
  • amount0Min': 0,
  • amount1Min': 0,

  • recipient - наш публичный адрес,
  • deadline - время в формате Unix после которого обмен не состоится, защиты от сэндвич атак.

Переходим к добавлению ликвидности на Uniswap на Python

Настройки смарт-контракта

Адрес контракта из Arbiscan:
UNISWAP_POOL_CONTRACT_ADDRESS = '0xC36442b4a4522E871399CD717aBDD847Ab11FE88'
ABI контракта из Arbiscan:
UNISWAP_POOL_ABI = '[{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH9","type":"address"},{"internalType":"address","name":"_tokenDescriptor_","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1","type":"uint256"}],"name":"Collect","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"uint128","name":"liquidity","type":"uint128"},{"indexed":false,"internalType":"uint256","name":"amount0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1","type":"uint256"}],"name":"DecreaseLiquidity","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"uint128","name":"liquidity","type":"uint128"},{"indexed":false,"internalType":"uint256","name":"amount0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1","type":"uint256"}],"name":"IncreaseLiquidity","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PERMIT_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"WETH9","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"baseURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"burn","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint128","name":"amount0Max","type":"uint128"},{"internalType":"uint128","name":"amount1Max","type":"uint128"}],"internalType":"struct INonfungiblePositionManager.CollectParams","name":"params","type":"tuple"}],"name":"collect","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"}],"name":"createAndInitializePoolIfNecessary","outputs":[{"internalType":"address","name":"pool","type":"address"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint128","name":"liquidity","type":"uint128"},{"internalType":"uint256","name":"amount0Min","type":"uint256"},{"internalType":"uint256","name":"amount1Min","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"internalType":"struct INonfungiblePositionManager.DecreaseLiquidityParams","name":"params","type":"tuple"}],"name":"decreaseLiquidity","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"amount0Desired","type":"uint256"},{"internalType":"uint256","name":"amount1Desired","type":"uint256"},{"internalType":"uint256","name":"amount0Min","type":"uint256"},{"internalType":"uint256","name":"amount1Min","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"internalType":"struct INonfungiblePositionManager.IncreaseLiquidityParams","name":"params","type":"tuple"}],"name":"increaseLiquidity","outputs":[{"internalType":"uint128","name":"liquidity","type":"uint128"},{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint256","name":"amount0Desired","type":"uint256"},{"internalType":"uint256","name":"amount1Desired","type":"uint256"},{"internalType":"uint256","name":"amount0Min","type":"uint256"},{"internalType":"uint256","name":"amount1Min","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"}],"internalType":"struct INonfungiblePositionManager.MintParams","name":"params","type":"tuple"}],"name":"mint","outputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint128","name":"liquidity","type":"uint128"},{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"permit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"positions","outputs":[{"internalType":"uint96","name":"nonce","type":"uint96"},{"internalType":"address","name":"operator","type":"address"},{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint128","name":"liquidity","type":"uint128"},{"internalType":"uint256","name":"feeGrowthInside0LastX128","type":"uint256"},{"internalType":"uint256","name":"feeGrowthInside1LastX128","type":"uint256"},{"internalType":"uint128","name":"tokensOwed0","type":"uint128"},{"internalType":"uint128","name":"tokensOwed1","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"refundETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowed","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowedIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"sweepToken","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"tokenOfOwnerByIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount0Owed","type":"uint256"},{"internalType":"uint256","name":"amount1Owed","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"uniswapV3MintCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"unwrapWETH9","outputs":[],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}]'

Инициализация контрактов

Помимо контракта Uniswap понадобятся контракты WETH и USDC для выдачи approve:
uniswap_pool_contract = web3.eth.contract(address=UNISWAP_POOL_CONTRACT_ADDRESS, abi=UNISWAP_POOL_ABI) 
Обратите внимание, что функция mint принимает кортеж:
usdc_contract = web3.eth.contract(address=USDC_CONTRACT_ADDRESS, abi=USDC_CONTRACT_ABI)
weth_contract = web3.eth.contract(address=WETH_CONTRACT_ADDRESS, abi=WETH_CONTRACT_ABI)

Approve токенов WETH USDC для контракта

WETH
weth_balance = weth_contract.functions.balanceOf(PUBLIC_ADDRESS).call()
Баланс в wei:

На момент написания статьи это 14,8 долларов.

Настройка транзакции для approve:

last_block = web3.eth.get_block('latest')
max_priority_fee_per_gas = web3.eth.max_priority_fee
base_fee = int(last_block['baseFeePerGas'] * 2)
max_fee_per_gas = base_fee + max_priority_fee_per_gas
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'maxPriorityFeePerGas': max_priority_fee_per_gas,
        'maxFeePerGas': max_fee_per_gas,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS)
        }
Расчет газа для транзакции и добавление в настройки:
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit) * 2})
Разрешение контракту в переменной UNISWAP_POOL_CONTRACT_ADDRESS на использование количества токенов, которые указаны в weth_balance:
approve_tx = weth_contract.functions.approve(UNISWAP_POOL_CONTRACT_ADDRESS, 
                                             weth_balance).build_transaction(transaction_settings)   
Подпись:
signed_transaction = web3.eth.account.sign_transaction(approve_tx, PRIVATE_KEY)
Отправка:
created_tx = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)
USDC
usdc_decimals = usdc_contract.functions.decimals().call()
usdc_balance = usdc_contract.functions.balanceOf(PUBLIC_ADDRESS).call() * 10 ** usdc_decimals

На балансе 50 USDC.

В пул ликвидности буду добавлять WETH на сумму 14,8 долларов и попробую указать полный баланс USDC, т.е. 50 долларов.

Настройка транзакции для approve:
last_block = web3.eth.get_block('latest')
max_priority_fee_per_gas = web3.eth.max_priority_fee
base_fee = int(last_block['baseFeePerGas'] * 2)
max_fee_per_gas = base_fee + max_priority_fee_per_gas
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'maxPriorityFeePerGas': max_priority_fee_per_gas,
        'maxFeePerGas': max_fee_per_gas,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS)
        }
Расчет газа для транзакции и добавление в настройки:
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit) * 2})
Разрешение контракту в переменной UNISWAP_POOL_CONTRACT_ADDRESS на использование количества токенов, которые указаны в usdc_balance:
approve_tx = usdc_contract.functions.approve(UNISWAP_POOL_CONTRACT_ADDRESS, 
                                             usdc_balance).build_transaction(transaction_settings)   
Подпись:
signed_transaction = web3.eth.account.sign_transaction(approve_tx, PRIVATE_KEY)
Отправка:
created_tx = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)

Добавление ликвидности Uniswap

Настройка параметров, которые будут переданы в функцию mint:
uniswap_pool_params = {
    'token0': WETH_CONTRACT_ADDRESS,
    'token1': USDC_CONTRACT_ADDRESS,
    'fee'3000,
    'tickLower': -887220,
    'tickUpper'887220,
    'amount0Desired': weth_balance,
    'amount1Desired': usdc_balance,
    'amount0Min'0,
    'amount1Min'0,
    'recipient': PUBLIC_ADDRESS,
    'deadline'int((datetime.datetime.now() + timedelta(seconds=60)).timestamp())
    }
Параметры транзакции:
last_block = web3.eth.get_block('latest')
max_priority_fee_per_gas = web3.eth.max_priority_fee
base_fee = int(last_block['baseFeePerGas'] * 2)
max_fee_per_gas = base_fee + max_priority_fee_per_gas
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'maxPriorityFeePerGas': max_priority_fee_per_gas,
        'maxFeePerGas': max_fee_per_gas,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS)
        }
Расчет газа для транзакции и добавление в настройки:
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit) * 2})
Запуск функции:
uniswap_supply_liquidity = (
                        uniswap_pool_contract.functions.mint((uniswap_pool_params))
                        .build_transaction(transaction_settings)  
                      )
Подпись:
signed_transaction = web3.eth.account.sign_transaction(uniswap_supply_liquidity, PRIVATE_KEY)
Отправка:
created_tx = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)
Несмотря на то, что я указал в настройках транзакции USDC на 50 долларов, контракт взял то количество USDC, которое равно WETH, т.е. 14,89 долларов. Очень удобно, что не нужно указывать значения 1 к 1, т.к. мне пришлось бы получать какие-то дополнительные данные о стоимости ETH в долларах, чтобы понять какое количество USDC точно надо указывать.

Пример 3 - Забрать ликвидность из пула Uniswap

За уменьшение (полное удаление) или увеличение отвечает тот же контракт, который использовался для добавления ликвидности. Выполняю транзакцию через Metamask, перехожу в сканер, смотрю Input Data, какие функции запускались во время multicall - функции, которая запускает несколько функций в одной транзакции.

Сначала deacreaseLiquidity, которая принимает параметры в кортеже:

Затем collect:
Далее есть еще функции - разворачивание WETH и еще какая-то sweepToken, не разобрался, за что она отвечает:), нам нужно только decreaseLiquidity и collect. Информация на скриншотах не относится к добавленной ликвидности в предыдущем примере.
decreaseLiquidity - описание функции в документации https://docs.uniswap.org/contracts/v3/guides/providing-liquidity/decrease-liquidity
Какие параметры:
  • tokenId - идентификатор nft-токена ликвидности, который был сминчен,
  • объем ликвидности,

следующие два параметра вроде бы служат защитой от проскальзывания, можно оставить по нулям:
  • amount0Min: 0,
  • amount1Min: 0,

  • deadline - время в формате Unix после которого обмен не состоится, для защиты от сэндвич атак.
Какие параметры:
  • tokenId - идентификатор nft-токена ликвидности, который был сминчен,
  • recipient - куда должны вернуться токены, то есть наш адрес,
  • amount0Max - значение первого токена в паре, можно ставить огромное число, чтобы забрать все,
  • amount1Max - значение второго токена.

Есть проблема

tokenId

Нам нужно значение tokenId, которое можно получить руками через URL, но программным способом из самого Uniswap получить его нельзя - среди Read функций нет такого контракта. В этом месте можно почувствовать особенности блокчейна, когда какой-либо сервис как Uniswap не имеет собственной базы данных, к которой можно обратиться по API.

На странице https://docs.uniswap.org/api/subgraph/overview указаны Enpoint оракула The Graph, который должен помочь найти нужные параметры. При попытках подключиться, становится понятно, что указанный Endpoint хранит информацию для главной сети Ethereum, нельзя получить информацию для L2 или EVM-чейнов:

Пример кода для получения id токена ликвидности в главной сети Ethereum:

GRAPH_ENDPOINT = 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3'
query = '''{
  positions(where: { owner: "0x51a017ec6f474cFc0b093642fDB71A50a7D4CBA8" }) {
    id
  }
}'''
response = requests.post(GRAPH_ENDPOINT, json={'query': query})

Все id токенов этого владельца.

Нашел информацию о том, как получить эти данные в видео BlockmanCodes.

Я находил другие эндпоинты для Arbitrum, но в них были другие схемы, и я не смог найти id токена ликвидности.

liquidity

Второй параметр в функции decreaseLiquidity. Ситуация чуть проще - есть Read-функция positions в смарт-контракте, которая даст информацию о предоставленной ликвидности, принимает tokeId:

Получение id токена ликвидности

После попыток с The Graph, перешел к боле очевидному варианту - получение id токена по API Arbiscan, к счастью в сканере есть такой метод. Эндпоинт:
URL = 'https://api.arbiscan.io/api'

Параметры

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

params = {
       'module''account',
       'action''tokennfttx',
       'contractaddress': UNISWAP_POOL_CONTRACT_ADDRESS,
       'address': PUBLIC_ADDRESS,
       'page'1,
       'offset'100,
       'startblock'0,
       'endblock'99999999,
       'sort''asc',
       'apikey''9XUZ65USBGYQ9ARBGGSQF9783INQPF9CX4G'
}
Запрос:
response = requests.get(URL, params=params)
result = json.loads(response.text)
Список NFT за все время, среди нужный нам параметр tokenID:
Вручную укажу индекс последнего токена:
token_id = result['result'][8]['tokenID']

Получение информации о позиции для параметра liquidity

Вызываю Read-функцию контракта и передаю в нее token_id:
liquidity_position = uniswap_pool_contract.functions.positions(int(token_id)).call()
Нужное значение стоит в списке восьмым - 314053988352:
liquidity = liquidity_position[7]

Теперь можно забрать ликвидность на Uniswap через Python

decreaseLiquidity

uniswap_decrease_liquidity_params = {
    'tokenId' : int(token_id),
    'liquidity' : liquidity,
    'amount0Min' : 0,
    'amount1Min' : 0,
    'deadline' : int((datetime.datetime.now() + timedelta(seconds=60)).timestamp())
}
Параметры транзакции:
last_block = web3.eth.get_block('latest')
max_priority_fee_per_gas = web3.eth.max_priority_fee
base_fee = int(last_block['baseFeePerGas'] * 2)
max_fee_per_gas = base_fee + max_priority_fee_per_gas
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'maxPriorityFeePerGas': max_priority_fee_per_gas,
        'maxFeePerGas': max_fee_per_gas,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS)
        }
Расчет газа для транзакции и добавление в настройки:
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit) * 4})
Запуск функции:
uniswap_decrease_liquidity = (
                        uniswap_pool_contract.functions.decreaseLiquidity((uniswap_decrease_liquidity_params))
                        .build_transaction(transaction_settings)  
                      )
Подпись:
signed_transaction = web3.eth.account.sign_transaction(uniswap_decrease_liquidity, PRIVATE_KEY)
Отправка:
created_tx = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)
Транзакция прошла, но токены еще не вернулись:

collect

amount0Max и amount1Max указываю такими же, какие они были в Input Data, когда я совершал ручную транзакцию через Metamask:
get_back_tokens_params = {
     'tokenId' : int(token_id),
     'recipient' : PUBLIC_ADDRESS,
     'amount0Max' : 340282366920938463463374607431768211455,
     'amount1Max' : 340282366920938463463374607431768211455
 }
Параметры транзакции:
last_block = web3.eth.get_block('latest')
max_priority_fee_per_gas = web3.eth.max_priority_fee
base_fee = int(last_block['baseFeePerGas'] * 2)
max_fee_per_gas = base_fee + max_priority_fee_per_gas
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'maxPriorityFeePerGas': max_priority_fee_per_gas,
        'maxFeePerGas': max_fee_per_gas,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS)
        }
Расчет газа для транзакции и добавление в настройки:
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit) * 4})
Запуск функции:
uniswap_get_back_tokens = (
                        uniswap_pool_contract.functions.collect((get_back_tokens_params))
                        .build_transaction(transaction_settings)  
                      )
Подпись:
signed_transaction = web3.eth.account.sign_transaction(uniswap_get_back_tokens, PRIVATE_KEY)
Отправка:
created_tx = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)
Токены вернулись из пула на мой адрес:

Пример удаления ликвидности через Multicall

Объединю две транзакции в одну - сделаю decreaseLiquidity и collect через multicall - функция, которую предоставляет контракт Uniswap, она принимает список с параметрами для разных функций в виде байт-кода:
Снова запущу переменные с параметрами для функций:
uniswap_decrease_liquidity_params = {
    'tokenId' : int(token_id),
    'liquidity' : liquidity,
    'amount0Min' : 0,
    'amount1Min' : 0,
    'deadline' : int((datetime.datetime.now() + timedelta(seconds=600)).timestamp())
 
}
get_back_tokens_params = {
     'tokenId' : int(token_id),
     'recipient' : PUBLIC_ADDRESS,
     'amount0Max' : 340282366920938463463374607431768211455,
     'amount1Max' : 340282366920938463463374607431768211455
 
}
Чтобы превратить данные в байт-код, использую функцию encodeABI, в нее передается название функции и параметры:
decreaseLiquidity_bytes = uniswap_pool_contract.encodeABI(fn_name='decreaseLiquidity',
                                                          args=[(uniswap_decrease_liquidity_params)])
collect_bytes = uniswap_pool_contract.encodeABI(fn_name='collect',
                                                args=[(get_back_tokens_params)])
Параметры транзакции:
last_block = web3.eth.get_block('latest')
max_priority_fee_per_gas = web3.eth.max_priority_fee
base_fee = int(last_block['baseFeePerGas'] * 2)
max_fee_per_gas = base_fee + max_priority_fee_per_gas
transaction_settings = {
        'chainId': web3.eth.chain_id,
        'maxPriorityFeePerGas': max_priority_fee_per_gas,
        'maxFeePerGas': max_fee_per_gas,
        'nonce': web3.eth.get_transaction_count(PUBLIC_ADDRESS)
        }
gasLimit = web3.eth.estimate_gas(transaction_settings)
transaction_settings.update({'gas'int(gasLimit) * 4})
Запуск функции:
uniswap_multicall = (
                        uniswap_pool_contract.functions.multicall([decreaseLiquidity_bytes,collect_bytes])
                        .build_transaction(transaction_settings)  
                      )
Подпись:
signed_transaction = web3.eth.account.sign_transaction(uniswap_multicall, PRIVATE_KEY)
Отправка:
created_tx = web3.eth.send_raw_transaction(signed_transaction.rawTransaction)

Эта статья будет дополнена другими примерами.
# Теги

Пишу статьи с Python кодом для автоматизации крипты. Телеграм - @rukidablkliki

© Crypto-Py.com