Реализация встроенного DApp-браузера в мобильном криптокошельке
Встроенный DApp-браузер — один из сложнейших компонентов криптокошелька. Он должен загружать произвольные веб-приложения, инжектировать провайдер window.ethereum, обрабатывать подписи транзакций и при этом не стать вектором атаки на активы пользователя.
Архитектура: WebView + провайдер
Основа браузера — нативный WebView с инжекцией JavaScript-провайдера. На iOS это WKWebView, на Android — WebView с addJavascriptInterface. Метавселенная MetaMask, Trust Wallet и Coinbase Wallet реализуют это одинаковым паттерном.
Провайдер window.ethereum — это объект который DApp ожидает увидеть в браузере. Он должен реализовывать EIP-1193 интерфейс: метод request(method, params) для всех RPC-запросов.
Схема работы:
DApp (JS) → window.ethereum.request({method: 'eth_sendTransaction'})
→ постMessage в нативный слой
→ нативный код показывает пользователю диалог подтверждения
→ пользователь одобряет/отклоняет
→ ответ возвращается в JS через postMessage
→ Promise разрешается в DApp
Инжекция провайдера на iOS (WKWebView)
Скрипт провайдера инжектируем через WKUserScript с injectionTime: .atDocumentStart. Критично — именно atDocumentStart, иначе DApp может проверить window.ethereum до инжекции и решить что кошелька нет.
let providerScript = loadProviderJS() // Читаем из бандла
let userScript = WKUserScript(
source: providerScript,
injectionTime: .atDocumentStart,
forMainFrameOnly: false
)
webView.configuration.userContentController.addUserScript(userScript)
webView.configuration.userContentController.add(self, name: "ethereum")
JS-провайдер отправляет сообщения через webkit.messageHandlers.ethereum.postMessage({...}). Нативный код получает в userContentController(_:didReceive:).
Ответы передаём обратно через webView.evaluateJavaScript("window.ethereum._resolveResponse(\(id), \(result))").
Android: addJavascriptInterface
webView.addJavascriptInterface(EthereumProvider(this), "AndroidEthereum")
webView.settings.javaScriptEnabled = true
На Android JS-интерфейс работает синхронно, что создаёт проблему: методы @JavascriptInterface не могут возвращать Promise. Обходим через callback-паттерн: JS вызывает AndroidEthereum.request(id, method, paramsJson), нативный код в итоге вызывает webView.evaluateJavascript("resolveCallback($id, $result)", null).
Важно: addJavascriptInterface потенциально опасен. Методы аннотированные @JavascriptInterface видны для ВСЕГО JavaScript на странице, включая вредоносные iframe. Только аннотируйте методы которые необходимы. Никогда не добавляйте интерфейс с широким API.
Реализация window.ethereum провайдера
Минимальная реализация поддерживает методы EIP-1193:
// Инжектируемый провайдер (упрощённо)
window.ethereum = {
isMetaMask: true, // многие DApp проверяют этот флаг
chainId: '0x1',
selectedAddress: null,
request: async function({ method, params }) {
return new Promise((resolve, reject) => {
const id = generateId();
pendingRequests[id] = { resolve, reject };
webkit.messageHandlers.ethereum.postMessage({ id, method, params });
});
},
on: function(event, handler) {
// chainChanged, accountsChanged, connect, disconnect
eventHandlers[event] = eventHandlers[event] || [];
eventHandlers[event].push(handler);
}
};
Методы которые нужно поддержать обязательно:
-
eth_requestAccounts— запрос доступа к адресу, показываем диалог -
eth_accounts— список подключённых адресов -
eth_chainId— текущая сеть -
eth_sendTransaction— отправка транзакции, требует подтверждения -
personal_sign— подписание сообщения -
eth_signTypedData_v4— подписание структурированных данных (EIP-712) -
wallet_switchEthereumChain— запрос смены сети
Безопасность: это главное
Изоляция сессии. Каждая DApp должна иметь отдельный cookie-jar и localStorage. Не позволяйте DApp A читать данные DApp B. На iOS — отдельные WKWebViewConfiguration и WKWebsiteDataStore для каждой вкладки.
Проверка URL перед инжекцией. Не инжектируйте провайдер на произвольные страницы — только на HTTPS, только на DApp домены из вашего белого списка, или с явным подтверждением пользователя.
Диалог подтверждения транзакции. Пользователь должен видеть: адрес контракта, сумму ETH (если есть), данные транзакции в читаемом виде (декодированные через ABI), оценку газа и итоговую стоимость в фиате. Не показывайте сырой hex.
eth_signTypedData_v4 (EIP-712). Это структурированные данные — DApp просит подписать типизированный объект. Нужно распарсить JSON schema и показать пользователю что именно подписывается в человекочитаемом виде. MetaMask делает это через парсинг types и message полей. Подписание вслепую — риск для пользователя.
Фишинг-защита. Проверяем SSL-сертификат, показываем URL в адресной строке которую пользователь не может скрыть, блокируем alert() и prompt() из JS (DApp не должна перехватывать нативные диалоги). На iOS обрабатываем webView(_:runJavaScriptAlertPanelWithMessage:) и заменяем нативным UIAlertController.
Multi-tab и история
Браузер с одной вкладкой — минимум. Реализуем:
- Вкладки с изолированными данными
- Историю просмотров (опционально, многие пользователи кошельков предпочитают конфиденциальность)
- Закладки для часто используемых DApp
- Список популярных DApp (curated) для onboarding
На iOS несколько WKWebView можно держать в памяти — они ленивые пока не видимы. На Android WebView тяжёлый, для экономии памяти уничтожаем WebView неактивной вкладки и восстанавливаем URL при возврате.
React Native и Flutter
В React Native используем react-native-webview с инжекцией через injectedJavaScriptBeforeContentLoaded и onMessage. Ограничение: injectedJavaScriptBeforeContentLoaded на Android инжектируется не синхронно — есть гонка условий. Решение: дублируем инжекцию через evaluateJavaScript в onLoadStart.
В Flutter — webview_flutter с addJavaScriptChannel и runJavaScript. Аналогичные ограничения на Android.
Производительность
WebView рендерит полноценный веб-контент — это ресурсоёмко. На устаревших Android-устройствах (WebView на базе Chromium 80) сложные DeFi DApp могут тормозить. Мониторим через WebViewClient.onPageStarted / onPageFinished, показываем прогресс-бар загрузки.
Предзагрузка WebView при старте приложения (создаём инстанс в фоне) сокращает cold-start время браузера с ~800ms до ~200ms.
Процесс разработки
- Базовый WebView с навигацией, адресной строкой, прогресс-баром
-
Инжекция провайдера и поддержка
eth_requestAccounts,eth_accounts,eth_chainId - Диалог транзакции — отображение, подписание, отправка через RPC
-
Расширенные методы —
personal_sign,eth_signTypedData_v4,wallet_switchEthereumChain - Безопасность — изоляция, фишинг-защита, аудит
- Multi-tab и UX-полировка
Сроки: базовый браузер с eth_sendTransaction — 3-4 недели. Полноценный браузер с multi-tab, EIP-712 отображением, security-аудитом — 2-3 месяца.







