Реализация встроенного DApp-браузера в мобильном криптокошельке
Встроенный DApp-браузер — один из сложнейших компонентов криптокошелька. Он загружает произвольные веб-приложения, внедряет провайдер window.ethereum, обрабатывает подписи транзакций и при этом не должен стать способом атаки на активы пользователя. Наш опыт в мобильной разработке и десятки успешно запущенных продуктов позволяют выполнить такую интеграцию под ключ с гарантией безопасности. Типичный кейс: пользователь открывает Uniswap в браузере кошелька, DApp проверяет наличие window.ethereum, вызывает eth_requestAccounts — и сразу видит список своих аккаунтов. Если инжекция провайдера выполнена неправильно, DApp либо не обнаружит кошелек, либо будет уязвим для фишинга. Пример из практики: в одном проекте мы обнаружили, что на Android провайдер инжектировался после загрузки страницы, и DApp не успевали зарегистрироваться, что приводило к ошибке "No Ethereum provider found". Решение — предзагрузка скрипта до onPageStarted.
Основа браузера — нативный WebView с инжекцией JavaScript-провайдера. На iOS это WKWebView, на Android — WebView с addJavascriptInterface. MetaMask, Trust Wallet и Coinbase Wallet реализуют этот паттерн одинаково. Провайдер window.ethereum должен реализовывать интерфейс EIP-1193: метод request(method, params) для всех RPC-запросов. DApp обращается к этому объекту, а нативный код обрабатывает запросы.
Архитектура и инжекция провайдера
Схема работы:
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.
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.
Сравнение платформ
| Параметр | iOS (WKWebView) | Android (WebView) |
|---|---|---|
| Инжекция кода | WKUserScript, инжект до загрузки | addJavascriptInterface, гонка условий |
| Асинхронность | через postMessage, native callback | требуется callback-паттерн |
| Безопасность | изоляция встроенная | требуется дополнительная проверка |
| Скорость загрузки | на 30–50% выше | зависит от версии Chromium |
Как обеспечить безопасность при инжекции провайдера?
Безопасность — главный приоритет. Основные меры:
- Изоляция сессии. Каждая DApp должна иметь отдельный cookie-jar и localStorage. Не позволяйте DApp A читать данные DApp B. На iOS — отдельные
WKWebViewConfigurationиWKWebsiteDataStoreдля каждой вкладки. - Фишинг-защита. Проверяем SSL-сертификат, показываем URL в адресной строке, которую пользователь не может скрыть, блокируем
alert()иprompt()из JS. На iOS обрабатываемwebView(_:runJavaScriptAlertPanelWithMessage:)и заменяем нативнымUIAlertController. -
eth_signTypedData_v4(EIP-712). Это структурированные данные — DApp просит подписать типизированный объект. Нужно распарсить JSON schema и показать пользователю, что именно подписывается в человекочитаемом виде. Подписание вслепую — риск для пользователя.
Почему важно изолировать сессии DApp?
Изоляция предотвращает утечку данных между DApp. Если пренебречь этим, вредоносное DApp сможет прочитать сохранённые пароли или ключи другого DApp. На практике мы используем отдельные WKWebsiteDataStore для каждой вкладки на iOS и отдельные каталоги для WebView на Android.
Реализация 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, wallet_switchEthereumChain.
Multi-tab и производительность
Браузер с одной вкладкой — минимум. Реализуем:
- Вкладки с изолированными данными
- Историю просмотров (опционально, многие пользователи кошельков предпочитают конфиденциальность)
- Закладки для часто используемых DApp
- Список популярных DApp (curated) для onboarding
На iOS несколько WKWebView можно держать в памяти — они ленивые, пока не видимы. На Android WebView тяжёлый, для экономии памяти уничтожаем WebView неактивной вкладки и восстанавливаем URL при возврате.
Производительность WebView зависит от платформы. На устаревших Android-устройствах (WebView на базе Chromium 80) сложные DeFi DApp могут тормозить. Мониторим через WebViewClient.onPageStarted/onPageFinished, показываем прогресс-бар. Предзагрузка WebView при старте приложения сокращает cold-start время с ~800 мс до ~200 мс.
Что входит в работу
| Этап | Что получаете |
|---|---|
| Базовый WebView | Навигация, адресная строка, прогресс-бар |
| Инжекция провайдера | Поддержка EIP-1193 методов: requestAccounts, accounts, chainId, sendTransaction, personal_sign, signTypedData_v4 |
| Диалог транзакции | Читаемое отображение, подписание, отправка через RPC |
| Multi-tab | Изолированные сессии, вкладки, история, закладки |
| Безопасность | Фишинг-защита, SSL-валидация, изоляция, аудит |
| Документация и код-ревью | Полная документация API, руководство по интеграции |
Процесс разработки
- Базовый 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 месяца. Стоимость рассчитывается индивидуально. Повторное использование компонентов позволяет сэкономить до 40% бюджета при типовой интеграции.
Получите консультацию по вашему проекту: мы оценим архитектуру и предложим оптимальное решение под ключ. Свяжитесь с нами, чтобы обсудить детали.







