Привет, Хабр! В данной статье изначально планировалось поделиться процессом написания выпускной работы, но что-то пошло не так и, в итоге, по чистой случайности получился фреймворк. Здесь я постараюсь описать основные принципы его работы, поделюсь предпосылками создания и приведу парочку примеров применения.
Предпосылки создания
За время работы с web3 мне удалось изучить немало источников о работе данной библиотеки, но ни в одном из них не приводилось лаконичных примеров инициализации смарт-контракта. Как правило, все how-to-guides сводились к примеру из документации.
from web3 import Web3
w3 = Web3(Web3.HTTPProvider('https://rpc.ankr.com/eth'))
address = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
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 = w3.eth.contract(address=address, abi=abi)
Выше был приведен пример инициализации контракта USDC на блокчейне эфира. Простой, сырой, никакой валидации, но вполне исчерпывающий. После проведенных манипуляций мы уже можем сделать contract call, чтобы получить нужные данные из контракта или совершить транзакцию.
Несмотря на полноту и понятность данного примера — его сложно масштабировать. Реализовать сложную логику становится в разы сложней, когда необходимо инициализировать несколько смарт-контрактов за раз и, к примеру, сохранять полученные данные в хранилище для последующей аналитики.
Business case
Сейчас постараюсь описать задачу, которую мы будем решать. Предположим, что требуется забирать данные о состоянии пула ликвидности. Матерые дата-инженеры, наверное, уже сели писать DAG, но подождите, сейчас не об этом. К состоянию пула мы отнесем данные о резервах токенов в пуле.
Значения резервов в пуле могут показаться аномально большими, да и понять, где тут USDC, а где WETH тоже довольно сложно.
А если необходимо смотреть композицию в нескольких пулах? Так еще и на разных сетках! Да-да, протокол тоже может меняться, но мало того — протоколы также могут отличаться по своей функциональности, так, на сегодняшний день уже существуют протоколы для займов, для обмена токенами, торговли деривативами и т.д. В общем и целом, сформировалась своя финансовая экосистема на базе блокчейна — это тоже стоит брать в расчет.
Что имеем?
Итак, на основании вышеописанного я выделил следующие параметры, которые необходимо учитывать при инициализации смарт-контракта:
Блокчейн и провайдер. Чтобы инициализировать нужный контракт, требуется определить на какой сетке он задеплоен. У каждой сетки — свой провайдер. В данной статье я буду использовать публичные ноды. Они бесплатные и как раз подходят для небольших проектов.
Протокол и его спецификация. Протокол, по сути, является dApp'ом (децентрализованное приложение). У каждого протокола своя уникальная архитектура, которая помогает реализовать соответственный функционал.
Адрес и ABI. Адрес — уникальный идентификатор. ABI (Application Binary Interface) — это в основном то, как вызываются функции в смарт-контракте и какие возвращаются данные.
Определив параметры, можно плавно перейти к реализации.
Пишем интерфейс для смарт-контракта
Имея все вышеперечисленные параметры, мы могли бы просто передавать их в конструктор класса, но я выделил следующие минусы такого подхода:
Нарушение принципов SOLID. Делать подключение к ноде внутри сущности смарт-контракта будет неправильно, так как сущность провайдера тоже является составной. Подключаться к ноде можно не только по HTTP-протоколу, но и по вебсокетам. Поэтому, мы вынесем его в отдельный класс и будем внедрять зависимость в сущность контракта.
Чрезмерная перегрузка конструктора. Перед непосредственной инициализацией контракта необходимо валидировать переданные параметры, проверить подключение к ноде, проверить адрес. Если все if'ы поместить в конструктор, то конструктор будет сильно перегружен, тем самым затрудняется процесс отладки и поиска багов.
Учитывая данные минусы, я решил, что подходящим решением может выступить Builder, который будет поэтапно принимать нужные параметры и производить соответствующую валидацию, будь то контракт или провайдер.
from typing import Dict, Any, Optional, Generic, overload, final
from abc import ABC
from web3 import Web3
from web3.eth import Eth
from web3.exceptions import ValidationError, CannotHandleRequest
from raffaelo.typings.providers.typing import ProviderType
class iCBC(ABC):
_ABI = None
def __init__(self, address: str, provider: Generic[ProviderType]) -> None:
self._address = address
self._provider = provider
self._contract = self.contract = None
@property
def contract(self) -> Eth.contract:
return self._contract
@contract.setter
def contract(self, *args, **kwargs) -> None:
self._contract = self.builder.build(key='address', value=self._address).build(key='provider', value=self._provider).connect().construct()
class Builder:
def __init__(self, abi: str) -> None:
self._options: Dict[str, Any] = dict()
self._abi: str = abi
@overload
def build(self, params: Dict[str, Any]) -> "iCBC.Builder":
...
@overload
def build(self, key: str, value: Any) -> "iCBC.Builder":
...
@final
def build(
self,
key: Optional[str] = None,
value: Optional[str] = None,
params: Optional[Dict[str, Any]] = None
) -> "iCBC.Builder":
def validate(k: str, v: Any) -> None:
if k == 'address':
if not Web3.isAddress(value=v):
raise ValidationError("Invalid address")
elif k == 'provider':
if not v.provider.isConnected():
raise CannotHandleRequest("Provider is down")
if isinstance(params, dict):
for k, v in params.items():
validate(k=k, v=v)
self._options[k] = v
elif isinstance(key, str):
validate(k=key, v=value)
self._options[key] = value
return self
@final
def connect(self) -> "iCBC.Builder":
if self._options.get('address'):
self._options['address'] = Web3.toChecksumAddress(value=self._options.get('address'))
return self
@final
def construct(self) -> Eth.contract:
return Web3(provider=self._options['provider'].provider).eth.contract(address=self._options['address'], abi=self._abi)
@property
def builder(self) -> Builder:
return self.Builder(abi=self._ABI)
Выше представлен код интерфейса для смарт-контракта, он же — iCBC
(Class Based Contract). Давайте разберем его по порядку и поймем, как он может помочь упростить разработку.
Казалось бы, а причем тут Spark?
По большей части, вдохновением для написания данного интерфейса послужил многим знакомый Apache Spark, а именно его имплементация builder'a в SparkSession.
Как происходит инициализация?
В конструктор класса передается адрес и внедряется зависимость в виде провайдера.
На 18-ой строчке происходит вызов сеттера для атрибута
contract
. Поведение сеттера всегда происходит одинаково, тем самым мы ограничиваем пользователя от экземпляра строителя, начиная выстраивать нужный ему контракт.Сеттер. Внутри сеттера идет обращение к атрибуту
builder
, который возвращает объектBuilder'a
. ABI передается дальше в конструктор строителя.У класса
Builder
, который определен прям внутри интерфейса, нужным образом перегружен методbuild
, который может получать в качестве параметров либо значения key — value, либо сразу словарик.Метод
build
поэтапно выстраивает нужный смарт-контракт, перед этим валидируя входящие данные с помощью методаvalidate
и сохраняя все в локальный словарик строителя.Далее вызывается метод
connect
, который приводит адрес контракта к checksum-адресу.И в конце вызывается метод
construct
, который и инициализирует нужный контракт, присваивая его атрибутуcontract
.
Аналогичным образом происходит инициализация провайдера.
Инструкция по применению
Данный интерфейс помогает автоматизировать инициализацию смарт-контракта, но основная его цель была решить проблему масштабируемости, чтобы можно было взаимодействовать с контрактами, как с объектами. Сейчас я постараюсь привести пример, который поможет лучше понять область его применения.
from raffaelo.interfaces.contracts.interface import iCBC
class ERC20TokenContract(iCBC):
_ABI = '[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]'
def name(self) -> str:
return self.contract.functions.name().call()
def totalSupply(self) -> int:
return self.contract.functions.totalSupply().call()
def decimals(self) -> int:
return self.contract.functions.decimals().call()
def balanceOf(self, address: str) -> int:
return self.contract.functions.balanceOf(address).call()
def symbol(self) -> str:
return self.contract.functions.symbol().call()
def allowance(self, owner: str, spender: str) -> int:
return self.contract.functions.allowance(owner, spender).call()
Собственно, вот вам и обертка для любого ERC-20 токена. В классе определены все read-методы, которые помогут извлечь нужную нам информацию.
from raffaelo.contracts.erc20.contract import ERC20TokenContract
from raffaelo.providers.http.provider import HTTPProvider
address = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
provider = HTTPProvider(uri='https://rpc.ankr.com/eth')
usdc = ERC20TokenContract(address=address, provider=provider)
А вот тот же пример инициализации USDC на блокчейне эфира. Теперь можно без труда дергать необходимые методы контракта, а если вдруг их забыли, то методом интроспекции провалиться внутрь класса и подсмотреть забытый метод.
Подведение итогов
Что в итоге? Мы написали интерфейс, который:
Ускоряет написание бизнес-логики. Имплементировать новые контракты становится в разы проще. Достаточно просто наследоваться от интерфейса, указать в качестве атрибута нужный ABI и определить методы смарт-контракта.
Улучшает качество и читаемость кода. Таким образом разработчику легче понять с каким классом объектов он работает. На мой взгляд — код действительно выглядит чище.
Повысили безопасность — теперь будет проще обнаружить баги.
Код проекта расположен тут. Там же описан пример, дающий ответ на задачу из бизнес-кейса.