Реализация отправки криптовалюты из мобильного кошелька
Экран отправки — один из самых критичных в мобильном кошельке. Здесь пользователь вводит адрес получателя, сумму и подтверждает транзакцию. Ошибка на любом из этих шагов — необратимая потеря средств. Поэтому логика отправки строится с явным упором на валидацию, защиту от случайного подтверждения и прозрачное отображение комиссий.
Валидация адреса до отправки
Самая частая причина потери средств — невалидный или некорректный адрес. Для Ethereum и EVM-совместимых сетей обязательна проверка checksum по EIP-55:
// iOS — web3swift
import web3swift
let address = EthereumAddress(inputString)
guard address != nil else { /* показать ошибку */ }
// Android — web3j
import org.web3j.crypto.WalletUtils
val isValid = WalletUtils.isValidAddress(inputAddress)
Для Bitcoin нужно отдельно разбирать формат — P2PKH, P2SH или bech32 (SegWit). Библиотека BitcoinKit (iOS) и bitcoinj (Android) покрывают все три. Solana-адреса — base58, 32 байта; SolanaSwift предоставляет PublicKey(string:) с выбросом исключения при некорректном вводе.
EVM-адреса в нижнем регистре и адреса с checksum — разные строки, но оба валидны. Отображать пользователю лучше checksum-версию.
Построение и подписание транзакции
Флоу отправки:
- Пользователь вводит адрес и сумму.
- Приложение запрашивает актуальный
gasPrice/maxFeePerGasчерезeth_gasPriceилиeth_feeHistory. - Оценивает
gasLimitчерезeth_estimateGasс параметрами транзакции — не захардкоживать 21000, если это не plain ETH transfer. - Показывает итоговую комиссию в USD по актуальному курсу.
- Пользователь подтверждает — приложение подписывает транзакцию приватным ключом локально.
- Отправка подписанного hex через
eth_sendRawTransaction.
Приватный ключ никогда не покидает устройство. Для хранения — iOS Keychain с kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, Android Keystore с KeyPairGenerator и флагом setUserAuthenticationRequired(true).
// Android — подписание через web3j
val credentials = Credentials.create(privateKey)
val rawTransaction = RawTransaction.createEtherTransaction(
nonce, gasPrice, gasLimit, toAddress, value
)
val signedMessage = TransactionEncoder.signMessage(rawTransaction, chainId, credentials)
val hexValue = Numeric.toHexString(signedMessage)
web3j.ethSendRawTransaction(hexValue).send()
UX подтверждения и защита от ошибок
Экран подтверждения должен содержать полный адрес получателя (не сокращённый), сумму, сеть и итоговую комиссию. Кнопку «Отправить» — не рядом с «Отмена», лучше вынести вниз с явным отступом. На iOS уместен UIImpactFeedbackGenerator при успешной отправке — тактильный отклик снижает тревожность.
После eth_sendRawTransaction приложение получает txHash. Статус транзакции отслеживается через eth_getTransactionReceipt в цикле с задержкой (polling каждые 3–5 секунд) или через WebSocket подписку eth_subscribe("newHeads"). Показывать пользователю ссылку на Etherscan / BscScan / Solscan — обязательно.
Типичные ошибки реализации
Подмена адреса из буфера обмена — реальный вектор атаки. Приложение должно сравнивать первые и последние 4 байта вставленного адреса с тем, что пользователь видит на экране, и при несоответствии — показывать предупреждение. Ряд кошельков дополнительно показывает визуальный идентикон адреса (Blockies или Jazzicon).
Nonce management: если пользователь отправил транзакцию с pending-статусом, следующая транзакция должна использовать nonce + 1. Иначе вторая транзакция зависнет или заменит первую. Хранить nonce локально, синхронизировать с eth_getTransactionCount(..., "pending") перед каждой отправкой.
Сроки: 3–5 дней: экран ввода с валидацией адреса и суммы, построение транзакции, подписание, отображение комиссий, экран подтверждения, трекинг статуса. ERC-20 transfer потребует дополнительного дня на ABI-encode данных transfer(address,uint256).







