Реализация истории транзакций в мобильном криптокошельке
История транзакций в крипто-кошельке — это не просто список записей из базы. Это агрегация данных с нескольких блокчейнов, парсинг raw транзакций через RPC-ноды или индексаторы, декодирование input data смарт-контрактов, расчёт fiat-эквивалентов на дату транзакции. При этом всё должно загружаться быстро и работать офлайн.
Источники данных транзакций
Получать транзакции напрямую через RPC-ноду (eth_getTransactionsByAddress не существует в стандартном JSON-RPC) — нельзя. Ethereum-нода не индексирует транзакции по адресу. Варианты:
Indexer API — Moralis, Alchemy, Etherscan API, The Graph. Moralis getWalletTransactions возвращает все транзакции по адресу с пагинацией. Etherscan API — бесплатный лимит 5 req/s, достаточно для MVP.
Собственный индексер — если нужна приватность или специфичная логика. The Graph позволяет написать subgraph с нужной схемой данных и запрашивать через GraphQL.
Multicall — для получения балансов токенов из одного запроса вместо N запросов.
На практике для multi-chain кошелька: Alchemy/Moralis для EVM-сетей, Solana RPC с getSignaturesForAddress для Solana, собственные модули для других сетей.
Структура данных и нормализация
Транзакции с разных сетей имеют разный формат. Нормализуем в единую модель:
class TransactionRecord {
final String hash;
final String chainId;
final TransactionType type; // send, receive, swap, approve, contract_call
final String fromAddress;
final String toAddress;
final BigInt amount; // в wei/lamports/satoshi
final String tokenSymbol;
final String? tokenAddress; // null для native currency
final int decimals;
final DateTime timestamp;
final TransactionStatus status; // confirmed, pending, failed
final BigInt? gasFee;
final double? fiatValueAtTime; // в USD по курсу на момент транзакции
final String? swapFromToken; // для DEX-свопов
final String? swapToToken;
}
fiatValueAtTime — отдельная задача. CoinGecko Historical API (/coins/{id}/history?date=DD-MM-YYYY) даёт цену токена на конкретную дату. Для USD-эквивалента при создании записи о транзакции — запрашиваем и кэшируем в локальной БД, потому что цены меняются.
Декодирование типа транзакции
Простой перевод ETH — input: "0x", to — адрес получателя. Но ERC-20 transfer выглядит как вызов контракта с input: "0xa9059cbb...". Нужно декодировать:
TransactionType detectTxType(String inputData, String toAddress, List<String> knownContracts) {
if (inputData == '0x' || inputData.isEmpty) return TransactionType.send;
final selector = inputData.substring(0, 10); // первые 4 байта
const selectors = {
'0xa9059cbb': TransactionType.tokenTransfer, // transfer(address,uint256)
'0x095ea7b3': TransactionType.approve, // approve(address,uint256)
'0x38ed1739': TransactionType.swap, // swapExactTokensForTokens (Uniswap v2)
'0x7ff36ab5': TransactionType.swap, // swapExactETHForTokens
};
return selectors[selector] ?? TransactionType.contractCall;
}
Для декодирования параметров ERC-20 transfer — парсим input: адрес получателя в байтах 4-35, сумма — байты 36-67.
Кэширование и пагинация
История транзакций — данные, которые растут в одну сторону (append-only, старые транзакции не меняются). Стратегия кэширования: сохраняем все загруженные транзакции в SQLite через drift, при обновлении запрашиваем только новые (от последнего известного блока).
Пагинация на UI: LazyColumn (Android Jetpack Compose) или ListView.builder (Flutter) с pagination_controller. При скролле вниз — подгружаем следующую страницу через cursor-based pagination (по block number или timestamp), не offset-based.
// Flutter — пагинация с курсором
Future<void> loadMoreTransactions() async {
if (_isLoading || !_hasMore) return;
_isLoading = true;
final oldestTx = _transactions.lastOrNull;
final newTxs = await repository.getTransactions(
address: walletAddress,
before: oldestTx?.timestamp,
limit: 20,
);
_transactions.addAll(newTxs);
_hasMore = newTxs.length == 20;
_isLoading = false;
notifyListeners();
}
Фильтрация и поиск
Фильтры по типу (отправить/получить/своп), по токену, по дате, по сети. Реализуем локально на закэшированных данных — быстро и без сетевых запросов. drift поддерживает сложные WHERE-запросы с индексами.
Поиск по адресу или хэшу транзакции — полнотекстовый поиск в SQLite через LIKE или FTS5 extension.
Статусы pending-транзакций
Отправленные транзакции сначала попадают в mempool со статусом pending. Нужно отслеживать их подтверждение: периодически проверяем eth_getTransactionReceipt для каждой pending транзакции, или подписываемся на WebSocket eth_subscribe("newHeads") и при каждом новом блоке проверяем pending список.
Что входит в работу
- Интеграция с indexer API (Moralis / Alchemy / Etherscan) или GraphQL subgraph
- Нормализация транзакций с поддержкой нескольких сетей
- Декодирование типов транзакций (transfer, approve, swap)
- Локальный кэш в SQLite с пагинацией
- Fiat-эквиваленты на дату транзакции
- Фильтрация и поиск
- Отслеживание pending транзакций
Сроки
Один блокчейн (EVM), базовый список с кэшем: 1–2 недели. Multi-chain с DEX-декодированием, фиатными эквивалентами и полной фильтрацией: 3–5 недель. Стоимость рассчитывается индивидуально.







