Python SDK
epic-python-sdk — типизированный асинхронный Python-клиент для узла, Owner API v3 кошелька и релея epicbox.
Управляет уже запущенным программным обеспечением Epic и не хранит ключей.
Версия 0.1.0 отсутствует на PyPI, и публичный API не заморожен. Устанавливайте из чекаута и фиксируйте коммит.
Состав пакета
Один wheel, три импортируемых пакета.
| Импорт | Предоставляет | Дополнения |
|---|---|---|
epic | NodeClient, WalletClient, EpicboxClient и команда epic. | none |
epictk | Загружает релизные бинарники, записывает конфиги, разворачивает цепочку и управляет процессами узла, кошелька и майнера. Команда epic-sdk. | [cli] |
epicdash | Веб-интерфейс поверх epictk: процессы, балансы, переводы и обозреватель блоков для локальной цепочки. | [cli,service] |
epic устанавливается без компилятора. Все криптографические примитивы поставляются из cryptography.
Установка
git clone https://github.com/blacktyger/epic-python-sdk
cd epic-python-sdk
uv sync # только клиент
uv sync --extra cli --extra service # плюс epic-sdk и дашборд
Python 3.12 или новее.
Первые вызовы
Каждый клиент является асинхронным контекстным менеджером. Подключается при входе и закрывается при выходе; конструктор не выполняет I/O.
Узел
import asyncio
from epic import NodeClient, Secret
async def main():
secret = Secret.from_env("EPIC_NODE_API_SECRET")
async with NodeClient("https://node.example:3413",
api_secret=secret) as node:
print(await node.get_height())
print(await node.is_synced(tolerance=10))
print(await node.get_pool_size())
asyncio.run(main())
NodeClient по умолчанию использует loopback на порту 3413.
is_synced() сравнивает локальную вершину цепочки с набором пиров, а tolerance — допустимое отставание локальной
вершины в блоках.
Кошелёк
from epic import Secret, WalletClient
url = "http://127.0.0.1:3420/v3/owner"
password = Secret.from_env("EPIC_WALLET_PASSWORD")
async with WalletClient(url, password=password) as wallet:
balance = await wallet.get_balance()
print(balance.spendable, balance.awaiting_confirmation)
for tx in await wallet.get_transactions():
print(tx.slate_id, tx.state, tx.amount)
print(await wallet.get_address())
Owner API находится на порту 3420 в каждой сети. get_address() возвращает адрес epicbox, включая порт.
Отправка
from epic import Amount, Secret, WalletClient
async with WalletClient(url, password=password,
can_spend=True) as wallet:
result = await wallet.send(
Amount.from_epic("1.5"),
"esYG...@epicbox.epiccash.com:443",
wait=True,
)
print(result.state)
Can spend funds send() выбирает и блокирует выходы (outputs) перед доставкой. Они остаются недоступными до
подтверждения перевода или пока cancel(slate_id) не освободит их. При wait=True вызов возвращает результат после
завершения второго раунда обмена или по истечении таймаута; значение по умолчанию — 120 секунд.
Перевод через epicbox состоит из двух раундов между двумя кошельками. Последовательность описана в разделе интерактивные транзакции.
Зашифрованная сессия
Owner API v3 согласовывает сессионный ключ через ECDH, запечатывает тело каждого запроса с помощью
AES-256-GCM и возвращает каждый результат в трёх конвертах. WalletClient выполняет рукопожатие при входе и
распаковывает каждый ответ. Оба варианта ниже читают один и тот же баланс.
- Owner API v3 напрямую
- Через клиент
# Рукопожатие: сжатый открытый ключ secp256k1, в открытом виде. Сессионный
# ключ — это x-координата общей точки.
post("init_secure_api", {"ecdh_pubkey": my_pubkey.hex()})
# open_wallet уже зашифрован, поэтому пароль передаётся в запечатанном
# теле, а не в параметрах JSON-RPC.
token = call_encrypted("open_wallet",
{"name": "default", "password": password})
# Каждый последующий вызов: запечатать внутренний запрос, отправить как
# encrypted_request_v3 с новым 12-байтовым nonce.
inner = {"jsonrpc": "2.0", "id": 1,
"method": "retrieve_summary_info",
"params": {"token": token, "refresh_from_node": True,
"minimum_confirmations": 10}}
ciphertext, nonce = aes_gcm_seal(session_key,
json.dumps(inner).encode())
reply = post("encrypted_request_v3",
{"nonce": nonce.hex(),
"body_enc": b64encode(ciphertext).decode()})
body = json.loads(aes_gcm_open( # конверт 1: запечатанное тело
session_key,
b64decode(reply["body_enc"]),
bytes.fromhex(reply["nonce"])))
result = body["result"] # конверт 2: JSON-RPC
ok = result["Ok"] # конверт 3: Ok / Err
# Сумма — строка в кавычках, в freemans.
spendable = int(ok[1]["amount_currently_spendable"]) / 100_000_000
Исполняемая версия этого клиента с пошаговым описанием рукопожатия находится в разделе кошелёк: подключение и чтение.
from epic import Secret, WalletClient
password = Secret.from_env("EPIC_WALLET_PASSWORD")
async with WalletClient(url, password=password) as wallet:
balance = await wallet.get_balance()
print(balance.spendable) # сумма в целых freemans
wallet.call(method, params) обращается к методу, который клиент ещё не оборачивает, через ту же сессию. Принимает allow_spend=True для
метода, перемещающего средства. Поверхность методов описана на странице wallet Owner API.
Суммы, трата и учётные данные
Суммы
1 EPIC равен 100,000,000 freeman. Amount хранит это целое число. from_epic() принимает str и Decimal.
Amount.from_epic("1.5") # 150_000_000 freemans
Amount.from_epic(Decimal("1.5"))
Amount.from_freemans(150_000_000)
Amount.from_epic(1.5) # AmountError
Трата
WalletClient выполняет трату только при создании с can_spend=True. Без него send(), finalize(), reshape_outputs() и call(..., allow_spend=True) вызывают NotSpendable.
Учётные данные
Пароли кошельков и API-секреты являются значениями Secret. Secret.from_env(name) читает одно из них из окружения.
password = Secret.from_env("EPIC_WALLET_PASSWORD")
print(password) # Secret(***)
log.info("opening", pw=password) # Secret(***)
Secret отображается как Secret(***) в repr, str, f-строках, событиях журнала, отрендеренных трассировках и
pickle. Команда epic читает учётные данные из окружения или из пути к файлу.
Какая поверхность принимает какие учётные данные — в разделе аутентификация.