Создаем десктопное приложение для анализа криптокошельков

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Приветствую форумчане, в прошлой статье я рассказал и показал, как сделать расширение для Google Chrome на JavaScript, и получил положительный отклик — статья вызвала интерес у пользователей. Поэтому немного пораскинув мозгами, я решил что почему бы и нет — вернемся в давно забытое детское ощущение, почувствуем себя скрипткиддис и запилим небольшой цикл статей со всякими полезными мини-тулзами. Какие-то из них будут в формате Proof of Concept, какие-то же наоборот будут готовы к работе почти из коробки. Основная моя задача здесь — показать, что программировать это не сложно, программировать может начать каждый и не обязательно для этого сразу целиться в убертехнологии и мегасложный продукт — можно просто писать небольшие программы, которые улучшают или упрощают жизнь коллег по цеху и на хлеб с маслом вам всегда будет хватать.

Сегодня займемся десктопным приложением. На форуме достаточно предложений по проверке сид-фраз, исходя из этого можно предположить, что есть спрос на инструменты для анализа и мониторинга кошельков. Поэтому статья для тех, кто эти сидки добывает (хоть из паблика) и не хочет ни с кем этой информацией делиться.

Перед тем, как приступим, опишу основной функционал приложения:
  1. Выбор кошелька из списка в текстовом документе
  2. Разные методы сбора данных (в зависимости от API)
  3. Асинхронная обработка большого количества адресов
  4. Сохранение результатов в JSON-формате
  5. Кроссплатформенность благодаря использованию Electron
  6. Отображение прогресса анализа в реальном времени
Как создавать директорию и файлы вы уже знаете, так что держите просто скрин:

Снимок экрана 2024-09-20 121737.png



Архитектура приложения состоит из нескольких ключевых компонентов:
  • Main process (main.js) - основной процесс Electron, отвечающий за создание окна приложения и обработку системных событий.
  • Renderer process (index.html, renderer.js) - процесс рендеринга, отвечающий за пользовательский интерфейс и взаимодействие с пользователем.
  • Preload script (preload.js) - скрипт, обеспечивающий безопасное взаимодействие между основным и рендерер процессами.
  • API интеграции - модули для работы с различными API (я выбрал Mobula и Moralis).
Начнем с разбора main.js - в Electron приложении отвечает за создание окон, управление жизненным циклом приложения и взаимодействием с нативными API операционной системы. Первым делом мы импортируем необходимые модули и создаем главное окно приложения:

JavaScript: Скопировать в буфер обмена
Код:
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const fs = require('fs').promises;
const axios = require('axios');
const { chromium } = require('playwright');
const Queue = require('better-queue');
const Moralis = require('moralis').default;
const { EvmChain } = require('@moralisweb3/common-evm-utils');

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      preload: path.join(__dirname, 'preload.js')
    },
  });

  mainWindow.loadFile('index.html');
}

Обратите внимание на настройки webPreferences: nodeIntegration: false - отключает прямой доступ к Node.js API из рендерер процесса для безопасности; contextIsolation: true - изолирует preload скрипт от рендерер процесса, а путь к скрипту и странице указываем через preload: path.join(__dirname, 'preload.js') и mainWindow.loadFile('index.html') соответственно.

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

JavaScript: Скопировать в буфер обмена
Код:
app.whenReady().then(() => {
  createWindow();

  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit();
});

Следом функции входного(.txt) и выходного(.json) файлов. Эти обработчики позволяют пользователю выбирать файлы для ввода адресов и сохранения результатов анализа. Использование dialog из Electron обеспечивает нативный опыт работы с файловой системой:

JavaScript: Скопировать в буфер обмена
Код:
ipcMain.handle('select-input-file', async () => {
  const result = await dialog.showOpenDialog(mainWindow, {
    properties: ['openFile'],
    filters: [{ name: 'Text Files', extensions: ['txt'] }],
  });
  if (result.canceled) {
    return null;
  }
  return result.filePaths[0];
});

ipcMain.handle('select-output-file', async () => {
  const result = await dialog.showSaveDialog(mainWindow, {
    filters: [{ name: 'JSON Files', extensions: ['json'] }],
  });
  if (result.canceled) {
    return null;
  }
  return result.filePath;
});

Чтобы можно было контролировать нагрузку и избегать превышения лимитов запросов API, я использовал очередь для обработки запросов. Эта очередь обрабатывает запросы последовательно, с возможностью повторных попыток в случае ошибок. Это особенно важно при работе с внешними API, где могут возникать временные сбои или ограничения на количество запросов:

JavaScript: Скопировать в буфер обмена
Код:
const requestQueue = new Queue(async (task, callback) => {
  try {
    const result = await processRequest(task);
    callback(null, result);
  } catch (error) {
    callback(error);
  }
}, { concurrent: 1, maxRetries: 3, retryDelay: 1000 });

async function processRequest(task) {
  const { method, address, apiKey } = task;
  switch (method) {
    case 'Mobula':
      return await processWalletMobula(address, apiKey);
    case 'Moralis':
      return await processWalletMoralis(address, apiKey);
    default:
      throw new Error('Неизвестный метод');
  }
}

Рассмотрим функционал работы с самими API. Принцип простейший – отправлять запросы с номером кошелька, обрабатывать полученные данные и возвращать результат в унифицированном формате. Я начал тестировать с тем, что попало под руку и где можно было без лишних проблем получить ключ. Начал с Mobula (https://mobula.io/apis), для получения ключа необходимо в нижнем левом углу экрана найти FREE API KEY, перейти по ссылке в документацию https://docs.mobula.io/introduction, там подробно описаны дальнейшие действия. На получение ключа ушло около 2-3 минут, сам сервис выдает лимитированные 10000 кредитов в сутки, но забегая вперед могу сказать, что панелька работает криво, количество кредитов в личном кабинет всегда 0, но и работает также медленно, как и считает.

Снимок экрана 2024-09-20 130417.png




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

Код: Скопировать в буфер обмена
Код:
({
  "data": {
    "total_wallet_balance": 123,
    "wallet": "<string>",
    "assets": [
      {
        "asset": {
          "data": {
            "id": 123,
            "name": "<string>",
            "symbol": "<string>",
            "contracts": [
              "<string>"
            ],
            "blockchains": [
              "<string>"
            ],
            "twitter": "<string>",
            "website": "<string>",
            "logo": "<string>",
            "price": 123,
            "market_cap": 123,
            "liquidity": 123,
            "volume": 123,
            "description": "<string>",
            "kyc": "<string>",
            "audit": "<string>",
            "total_supply_contracts": [
              "<string>"
            ],
            "total_supply": 123,
            "circulating_supply": 123,
            "circulating_supply_addresses": [
              "<string>"
            ],
            "discord": "<string>",
            "max_supply": 123,
            "chat": "<string>"
          }
        },
        "price": 123,
        "estimated_balance": 123,
        "token_balance": 123,
        "cross_chain_balances": {}
      }
    ]
  },
  "lastUpdated": {}
}
)

Сама же функция выглядит следующим образом:

JavaScript: Скопировать в буфер обмена
Код:
async function processWalletMobula(address, apiKey) {
  try {
    const response = await axios.get(`https://api.mobula.io/api/1/wallet/portfolio?wallet=${address}`, {
      headers: { 'Authorization': `Bearer ${apiKey}` }
    });
    const balance = response.data.data.total_wallet_balance;
    return { address, balance };
  } catch (error) {
    throw error;
  }
}

Куда более интересней представлена функция processWalletMoralis, которая использует Moralis API для получения детальной информации о балансах кошелька на различных блокчейнах. Это позволяет получить комплексную картину активов пользователя across different chains. На получение ключа ушло около 10 минут, здесь все просто, но пришлось повозиться с документацией, чтобы понять функционал. Заходим на https://developers.moralis.com, жмем Get API Key, регистрируемся и готово. Панелька интуитивно куда более удобная, считает корректно, API работает быстро, но за один запрос может съедать до тысячи местных кредитов. В бесплатном варианте в сутки выдается 40к кредитов, но за 70 деревянных в месяц можно увеличить лимит до 100млн/месяц, а если языком поработать – можно и в космос улететь.


Снимок экрана 2024-09-20 130822.png



Снимок экрана 2024-09-20 131338.png



В документации меня интересовал Get Wallet Net Worth. Основными преимуществами являлись пробив по необходимым блокчейнам и примеры кода/ответов. Блокчейны для анализа там же в доках можете посмотреть, в соответствующей графе (для самых ленивых скрин):

Снимок экрана 2024-09-20 132235.png



Выбрал несколько для теста (по коду понятно какие именно) и поехали:


JavaScript: Скопировать в буфер обмена
Код:
async function processWalletMoralis(address, apiKey) {
  try {
    const response = await Moralis.EvmApi.wallets.getWalletNetWorth({
      chains: [
        EvmChain.ETHEREUM,
        EvmChain.POLYGON,
        EvmChain.FANTOM,
        EvmChain.ARBITRUM,
        EvmChain.BSC,
        EvmChain.AVALANCHE,
        EvmChain.BASE,
        EvmChain.OPTIMISM
      ],
      excludeSpam: false,
      excludeUnverifiedContracts: false,
      address: address
    });

    const result = response.raw;
    
    return {
      address,
      total_networth_usd: result.total_networth_usd,
      chains: result.chains.map(chain => ({
        chain: chain.chain,
        native_balance: chain.native_balance,
        native_balance_formatted: chain.native_balance_formatted,
        native_balance_usd: chain.native_balance_usd,
        token_balance_usd: chain.token_balance_usd,
        networth_usd: chain.networth_usd
      }))
    };
  } catch (error) {
    console.error(`Error processing wallet ${address} with Moralis API:`, error);
    return {
      address,
      error: error.message
    };
  }
}

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

JavaScript: Скопировать в буфер обмена
Код:
ipcMain.handle('process-data', async (event, inputFile, outputFile, apiKey, method) => {
  try {
    const walletAddresses = (await fs.readFile(inputFile, 'utf-8')).split('\n').map(address => address.trim());
    const results = [];
    const startTime = Date.now();
    let processedRequests = 0;

    if (method === 'Moralis') {
      await Moralis.start({ apiKey: apiKey });
    }

    for (const address of walletAddresses) {
      await new Promise((resolve, reject) => {
        requestQueue.push({
          method,
          address,
          apiKey
        }, (err, result) => {
          if (err) reject(err);
          else {
            results.push(result);
            processedRequests++;
            const progress = (processedRequests / walletAddresses.length) * 100;
            const elapsedTime = (Date.now() - startTime) / 1000;
            const requestsPerMinute = (processedRequests / elapsedTime) * 60;
            mainWindow.webContents.send('progress-update', {
              progress,
              processedRequests,
              elapsedTime,
              requestsPerMinute
            });
            resolve();
          }
        });
      });
    }

    await fs.writeFile(outputFile, JSON.stringify(results, null, 2), 'utf-8');
    return 'Анализ успешно завершен!';
  } catch (error) {
    return `Ошибка: ${error.message}`;
  } finally {
    if (method === 'Moralis') {
      await Moralis.stop();
    }
  }
});

С функионалом main.js закончили, переходим к preload.js. Код сам по себе – пара строк, но сам скрипт играет ключевую роль в обеспечении безопасности приложения, предоставляя контролируемый интерфейс между основным и рендерер процессами. Я использовал contextBridge для создания безопасного API, доступного из рендерер процесса. Это позволяет контролировать, какие именно функции основного процесса доступны для использования в пользовательском интерфейсе:

JavaScript: Скопировать в буфер обмена
Код:
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  selectInputFile: () => ipcRenderer.invoke('select-input-file'),
  selectOutputFile: () => ipcRenderer.invoke('select-output-file'),
  processData: (inputFile, outputFile, apiKey, method) =>
    ipcRenderer.invoke('process-data', inputFile, outputFile, apiKey, method),
  onProgressUpdate: (callback) => ipcRenderer.on('progress-update', (_event, value) => callback(value))
});

Пользовательский интерфейс приложения сделал с простой структурой: возможность выбрать входной файл с адресами кошельков, указать выходной файл для результатов, выбрать метод анализа (Mobula или Moralis) и ввести API ключ.

HTML: Скопировать в буфер обмена
Код:
<html>
<head>
    <title>Анализатор криптокошельков</title>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; }
        input, select, button { margin: 10px 0; }
        #progressBar { width: 100%; background-color: #ddd; }
        #progressBar > div { height: 30px; background-color: #4CAF50; text-align: center; line-height: 30px; color: white; }
    </style>
</head>
<body>
    <h1>Анализатор криптокошельков</h1>
    <div>
        <label for="inputFile">Входной файл (адреса кошельков):</label>
        <input type="text" id="inputFile" readonly>
        <button onclick="selectInputFile()">Выбрать файл</button>
    </div>

    <div>
        <label for="outputFile">Выходной файл (JSON):</label>
        <input type="text" id="outputFile" readonly>
        <button onclick="selectOutputFile()">Выбрать файл</button>
    </div>

    <div>
        Метод сбора данных:
        <select id="method">
          <option value="Mobula">Mobula API</option>
          <option value="Moralis">Moralis API</option>
        </select>
    </div>

    <div>
        <label for="apiKey">Ключ API:</label>
        <input type="text" id="apiKey">
    </div>

    <button onclick="startAnalysis()">Начать анализ</button>

    <div id="progressBar"><div></div></div>
    <div id="statusLabel"></div>

    <script>
        let inputFile, outputFile;

        async function selectInputFile() {
            inputFile = await window.electronAPI.selectInputFile();
            document.getElementById('inputFile').value = inputFile || '';
        }

        async function selectOutputFile() {
            outputFile = await window.electronAPI.selectOutputFile();
            document.getElementById('outputFile').value = outputFile || '';
        }

        async function startAnalysis() {
            const apiKey = document.getElementById('apiKey').value;
            const method = document.getElementById('method').value;
            
            if (!inputFile || !outputFile) {
                alert('Пожалуйста, выберите входной и выходной файлы');
                return;
            }

            const result = await window.electronAPI.processData(inputFile, outputFile, apiKey, method);
            document.getElementById('statusLabel').innerText = result;
        }

        window.electronAPI.onProgressUpdate((data) => {
            const progressBar = document.getElementById('progressBar').firstChild;
            progressBar.style.width = `${data.progress}%`;
            progressBar.innerText = `${data.progress.toFixed(2)}%`;
            
            document.getElementById('statusLabel').innerText =
                `Обработано ${data.processedRequests} запросов за ${data.elapsedTime.toFixed(2)} секунд.
                 Скорость: ${data.requestsPerMinute.toFixed(2)} запросов/мин`;
        });
    </script>
</body>
</html>

Снимок экрана 2024-09-20 140427.png



renderer.js - скрипт обрабатывает нажатие на кнопку "Начать анализ", собирает необходимые данные из пользовательского ввода и вызывает функцию processData через API, предоставленный preload скриптом, обновляет прогресс-бар на основе обновлений, получаемых от основного процесса:

JavaScript: Скопировать в буфер обмена
Код:
document.getElementById('startAnalysis').addEventListener('click', async () => {
    const inputFile = document.getElementById('inputFile').files[0].path;
    const outputFile = document.getElementById('outputFile').value;
    const apiKey = document.getElementById('apiKey').value;
    const fullAnalysis = document.getElementById('fullAnalysis').checked;
    const method = document.getElementById('apiMethod').value;

    document.getElementById('statusLabel').textContent = 'Анализ начат...';
    
    try {
        const result = await window.electronAPI.processData(inputFile, outputFile, apiKey, fullAnalysis, method);
        document.getElementById('statusLabel').textContent = result;
    } catch (error) {
        document.getElementById('statusLabel').textContent = `Ошибка: ${error.message}`;
    }
});

window.electronAPI.onProgressUpdate((progress) => {
    document.querySelector('#progressBar > div').style.width = `${progress}%`;
});

Что же показали нам тесты? Я зашел на DeBank, выбрал 10 первых попавшихся кошельков, залил в текстовый документ.

1727091582690.png



Сначала поехала Mobula:

Снимок экрана 2024-09-20 143104.png



Скорость загрузки 0.45 запрос/минута. На 10 запросов реально ушло около 1241 секунда = больше 20 минут (скорость космическая). На скрине не успел поймать время (сами понимаете - нужна реакция), поэтому не видно результатов, рекомендую поверить на слово, а можете смело самостоятельно протестировать. Результат:

Снимок экрана 2024-09-23 145334.png



Слегка поиграв с тестами Mobula, начал пробовать Ankr, Zapper API – плюс минус аналогичный результат (изначально я внес в приложение эти API, когда собирал архитектуру, пробовал Debank по HTML парсингу пихнуть, как метод сбора данных, но достойного результата по скорости не было, поэтому удалено безвозвратно). Переборов в себе желание воспользоваться сочетанием клавиш shift+delete для корневой папки, добрался до Moralis. Ожиданий особо не было, но результат оказался приятным: 10 запросов обработались меньше чем за 25 секунд (1500 запросов/час), а это почти в 50 раз быстрее. Конечно, я изначально целился в результат около 1000 запросов в минуту, но практика и тесты показали, что за «спасибо» тебе никто таких объемов не даст (хотя если кто знает – сообщите). А для мелких объемов 5000-10000 запросов и домашнего ознакомительного пользования решил, что пойдет.


Снимок экрана 2024-09-20 144106.png



При этом ответы были подробными по каждому из блокчейнов, чтобы не грузить в основном тексте статьи, файл с ответами тоже добавлю в архив, пока пример одного кошелька на скрине:


1727092607144.png



Чем же пришлось заплатить за такие скорость и качество? Лимитом в панельке – съело с запасом, хотя ответ выдал по всем запрашиваемым кошелькам.

Снимок экрана 2024-09-20 145404.png



Да, приложение сразу не сортирует в списки «есть бабки»/«пустой кош», но это точно быстрее, чем ручками проверять. А если учесть, что написалось приложение буквально за два вечера под сериальчик, то результат считаю приемлемым.

Бонусом закину программу для генерации сидфраз, которую писал забавы ради в прошлом году. Выглядит она следующим образом:

Снимок экрана 2024-09-20 151401.png



Функционал простой:
  1. Скачиваешь 2048 волшебных слов,
  2. Добавляешь их в текстовый документ,
  3. Этот документ загружаешь в программу,
  4. Выбираешь количество слов, участвующих в составлении сидфраз,
  5. Выбираешь длину последовательности сидфразы
  6. Нажимаешь «Сгенерировать последовательности»
  7. Выбираешь выходной файл
  8. Наслаждаешься работой железного друга
Снимок экрана 2024-09-20 151802.png



Скорость генерации 100к/секунду. Есть функционал выбрать определенное слово из списка (например, если вы знаете одно или несколько слов сидфразы). Есть также возможность генерировать по стандартам BIP0039, в этом случае сначала рассчитывается контрольное слово, потом генерируется сидфраза. Общее количество комбинаций выводится после выбора количества слов и длины последовательности. Такое запредельное количество сгенерировать, сохранить и как следствие монетизировать, сможет только машина с огромными вычислительными мощностями (или команда). Для изучающих Python будет полезно, для тех, кому нечем заняться – весело поиграть со словами из своих сидфраз.

На сегодня все, уважаемые форумчане! Архив с файлами программ и результатами тестов во вложении. Кому было полезно и вызвало интерес – лайкайте, оставляйте комментарии, задавайте вопросы. Если у вас уже есть проплаченный API с отсутствием лимитов по запросам - можете интегрировать и пользоваться!

Patr1ck специально для XSS.is.
 
Сверху Снизу