Реалізація вбудованого 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'})
→ postMessage до нативного шару
→ нативний код показує діалог підтвердження
→ користувач одобряє/відхиляє
→ відповідь повертається до 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 схему й покажіть користувачам що саме вони підписують у людиночитаному вигляді. MetaMask робить це через парсинг полів types та message. Підписання без розуміння — ризик для користувача.
Захист від фішингу. Перевіріть SSL-сертифікати, показуйте URL в адресній строці, яку користувачі не можуть сховати, блокуйте alert() та prompt() з JS (DApp не повинна перехоплювати нативні діалоги). На iOS обробіть webView(_:runJavaScriptAlertPanelWithMessage:) і замініть на нативний UIAlertController.
Multi-tab та історія
Браузер з однією вкладкою — мінімум. Реалізуйте:
- Вкладки з ізольованими даними
- Історія перегляду (опціонально; багато користувачів гаманців переважають конфіденційність)
- Закладки для часто використовуваних DApp
- Кураторський список популярних DApp для 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 місяці.







