web3_webview 1.0.10
web3_webview: ^1.0.10 copied to clipboard
web3_webview is a powerful bridge between a DApp running inside a WebView and your Flutter application, enabling secure and seamless two-way communication between them
web3_webview #
web3_webview is a Flutter bridge between a DApp running inside an InAppWebView and your host app. It injects an EIP-1193 / EIP-6963 compliant window.ethereum provider (via ethers.min.js + generated script) and routes all wallet operations – connect, sign, sendTransaction, chain management – to native Dart dialogs and web3dart.
Security-first fork: private-key validation, XSS-safe JS injection, EIP-1193 error codes,
PermissionRequestdefault-deny, non-blockingeth_sendTransaction, and correct EIP-712 padding. See Security.
Architecture #
flowchart LR
DApp -- window.ethereum.request --> JSBridge[JS Provider\nprovider_script.dart]
JSBridge -- flutter_inappwebview callHandler\nethereumRequest --> WebView[Web3WebView\nweb3_webview_eip1193.dart]
WebView --> Provider[EthereumProvider\nsingleton + Web3Client]
Provider --> Signing[SigningHandler\nEIP-191 / EIP-712]
Provider --> Tx[TransactionHandler\nsend + estimate + gas]
Provider --> Dialog[WalletDialogService\nbottom sheet]
Provider -- jsonEncode + evaluateJavascript --> JSBridge
Core modules:
| Module | File | Responsibility |
|---|---|---|
Web3WebView |
lib/web3_webview_eip1193.dart |
Wraps flutter_inappwebview, injects ethers.min.js + provider script at AT_DOCUMENT_START (Android re-inject on onLoadStart), validates privateKey, shows initError state, forwards onPermissionRequest as DENY by default |
EthereumProvider |
lib/ethereum/ethereum_provider.dart |
Singleton, Web3Client + Client lifecycle, handleRequest dispatcher, 12s eth_blockNumber cache, chain registry, event emit via jsonEncode |
ProviderScriptGenerator |
lib/provider/provider_script.dart |
Generates JS EthereumProvider extends EventTarget, jsonEncode-escapes chainId/accounts/isConnected/info, handles request/send/sendAsync, EIP-6963 announce |
SigningHandler |
lib/signing/signing_handler.dart |
personal_sign/eth_sign/eth_signTypedData* with full EIP-712 v3/v4 validation, struct hashing, 32-byte padding for bool/address/bytesN |
TransactionHandler |
lib/transaction/transaction_handler.dart |
Validate to/value/data, estimateGas + 20% BigInt buffer, signTransaction(chainId), sendRawTransaction returns hash immediately (EIP-1193), optional waitForConfirmation |
WalletDialogService |
lib/ethereum/wallet_dialog_service.dart |
Bottom sheets for connect/sign/tx/switch/add, requestFrom = controller.getUrl().host, WalletDialogTheme |
Key Features #
- Wallet Connection – user-confirmed
eth_requestAccounts; returns[]oneth_accountsuntil connected - DApp Integration –
window.ethereum+window.web3.currentProvider,isMetaMask=true, EIP-6963 discovery - Transaction Lifecycle – confirmation dialog,
eth_estimateGasbuffered, non-blocking hash return, fire-and-forget receipt polling (opt-in blocking) - Message Signing –
personal_sign(EIP-191 prefix),eth_sign(raw keccak),eth_signTypedData/v1/v3/v4(EIP-712) - Chain Management –
wallet_switchEthereumChain/wallet_addEthereumChainwith idempotency checks - Two-way Bridge –
flutter_inappwebviewcallHandler↔evaluateJavascript, XSS-safe viajsonEncode - Multi-chain – any EVM via
NetworkConfig
Requirements #
- Flutter
>=3.24.0, Dart^3.5.0 flutter_inappwebview ^6.1.5,web3dart ^2.7.3
Installation #
dependencies:
web3_webview: ^1.0.9
import 'package:web3_webview/web3_webview.dart';
Quick Start #
privateKey is required and must be a 64-hex (32 bytes) string, with or without 0x. The widget validates it on initState and renders an inline error instead of crashing.
Do not hardcode keys. Load from
flutter_secure_storage/ Keychain / Keystore and clear on lock. See Security.
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:web3_webview/web3_webview.dart';
final ethMainnet = NetworkConfig(
chainId: '0x1',
chainName: 'Ethereum Mainnet',
nativeCurrency: NativeCurrency(name: 'Ether', symbol: 'ETH', decimals: 18),
rpcUrls: ['https://mainnet.infura.io/v3/YOUR_KEY'],
blockExplorerUrls: ['https://etherscan.io'],
);
final bscMainnet = NetworkConfig(
chainId: '0x38',
chainName: 'BNB Smart Chain',
nativeCurrency: NativeCurrency(name: 'BNB', symbol: 'BNB', decimals: 18),
rpcUrls: ['https://bsc-dataseed.binance.org'],
blockExplorerUrls: ['https://bscscan.com'],
);
class DappScreen extends StatefulWidget {
const DappScreen({super.key});
@override State<DappScreen> createState() => _DappScreenState();
}
class _DappScreenState extends State<DappScreen> {
String? _pk;
@override void initState() {
super.initState();
const storage = FlutterSecureStorage();
storage.read(key: 'evm_pk').then((v) => setState(() => _pk = v));
}
@override Widget build(BuildContext context) {
if (_pk == null) return const Center(child: CircularProgressIndicator());
return Web3WebView(
web3WalletConfig: Web3WalletConfig(
privateKey: _pk!, // required, 0x + 64 hex
currentNetwork: ethMainnet,
supportNetworks: [ethMainnet, bscMainnet],
name: 'MyDApp Wallet',
id: 'com.example.mydapp',
// optional theming
dialogTheme: WalletDialogTheme(primaryColor: Color(0xFF3B82F6)),
onError: (method, params, message) => debugPrint('[$method] $message'),
),
initialUrlRequest: URLRequest(url: WebUri('https://metamask.github.io/test-dapp/')),
// SECURITY: override only to grant selectively
onPermissionRequest: (ctrl, req) async => PermissionResponse(
resources: req.resources,
action: PermissionResponseAction.DENY, // default in lib
),
);
}
}
Handling Web3WalletConfig #
| Field | Type | Required | Notes |
|---|---|---|---|
privateKey |
String |
yes | 0x + 64 hex or 64 hex; validated with -32602 on invalid, Web3WebView shows inline error |
currentNetwork |
NetworkConfig? |
no | defaults to 0x1 |
supportNetworks |
List<NetworkConfig>? |
no | defaults to [eth, bsc] |
name/icon/id |
String? |
no | EIP-6963 info (icon is data-URI), id → rdns |
dialogTheme |
WalletDialogTheme? |
no | colors, text styles, paddings |
onError |
void Function(JsonRpcMethod, List?, String)? |
no | invoked before JS error is thrown, receives EIP-1193 code in message |
NetworkConfig { chainId (0x hex), chainName, nativeCurrency?, rpcUrls, blockExplorerUrls? }
Supported JSON-RPC #
EthereumProvider.handleRequest (lib/ethereum/ethereum_provider.dart:110) dispatches:
| Method | Params | Behaviour |
|---|---|---|
eth_requestAccounts |
[] |
Bottom sheet → connect + accountsChanged events, returns [address] or 4001 |
eth_accounts |
[] |
[] until connected |
eth_chainId / net_version |
[] |
state.chainId |
eth_blockNumber |
[] |
12s cached HexUtils.numberToHex(blockNumber) |
eth_call |
[tx, block] |
TransactionHandler.handleTransaction (no value) |
eth_sendTransaction |
[tx] |
Dialog → sign+send → hash immediately, polling fire-and-forget |
eth_getBalance |
[addr, tag] |
getBalance → hex |
eth_getBlockByNumber/Hash |
[id, withTx] |
makeRPCCall → Map? |
eth_getTransactionByHash/Receipt |
[hash] |
makeRPCCall → Map? |
eth_getCode/StorageAt/TransactionCount |
[...] |
getCode/getStorage/getTransactionCount or RPC fallback |
eth_gasPrice / eth_estimateGas |
[]/[tx] |
hex; estimate buffered +20% via BigInt |
personal_sign |
[hexMsg, addr, pw?] |
dialog shows decoded UTF-8, signs with signPersonalMessageToUint8List |
eth_sign |
[addr, hexMsg] |
raw keccak256 |
eth_signTypedData[*_v1/v3/v4] |
[addr, typed] |
EIP-712 with domain/type validation |
wallet_switchEthereumChain |
[{chainId}] |
idempotent, dialog if needed → chainChanged |
wallet_addEthereumChain |
[NetworkConfig] |
dialog → add + auto-switch |
wallet_getPermissions |
[] |
['eth_accounts','eth_chainId','personal_sign'] |
wallet_revokePermissions |
[] |
true |
| other | throws UnsupportedMethodException(4200) |
Unknown method surfaces to JS as {code:4200, message} and to onError.
Error codes (lib/exceptions.dart:2) #
WalletException.code follows EIP-1193 + JSON-RPC:
4001– user rejected4100– unauthorized4200– unsupported method4900– disconnected-32601– method not found-32602– invalid params-32603– internal
Web3WebView (lib/web3_webview_eip1193.dart:887) maps WalletException.code into the thrown {code,message} consumed by provider_script.dart:127 _processError.
Permissions & WebView #
onPermissionRequestdefaults toDENY(lib/web3_webview_eip1193.dart:1019). OverrideWeb3WebView.onPermissionRequestto grant selectively, neverGRANTall.shouldInterceptRequest/onReceivedServerTrustAuthRequestare forwarded; implement cert pinning in host if needed.- Provider injected as two
UserScriptatAT_DOCUMENT_START; on Android re-evaluated inonLoadStart.
Signing details (lib/signing/signing_handler.dart:1) #
personal_signaccepts0xhex or UTF-8, decodes for UI, signs prefixed.eth_signsignskeccak256(hexOrUtf8)directly (dangerous – dialog still shown).- Typed data:
typesmust containEIP712Domain; checksprimaryType,domainwhitelist (name,version,chainId,verifyingContract,salt), circular deps, array lengths, and ABI 32-byte padding forbool/address/bytesN/uint/int.
Transaction details (lib/transaction/transaction_handler.dart:1) #
- Requires
to(0x42 chars), validatesvalue/dataas0xhex. getTransactionCount(from)as nonce,estimateGas+20% with BigInt (*120/100),getGasPrice()orgasPriceoverride.handleTransaction({waitForConfirmation=false})– default returns hash; settrueto block up to 30×10s polling and throw onreceipt.status==falseor timeout.
Theming #
WalletDialogTheme(
primaryColor: Color(0xFF3B82F6),
textColor: Color(0xFF1F2937),
borderRadius: 16,
dialogPadding: EdgeInsets.all(24),
buttonConfirmStyle: ButtonConfig(backgroundColor: ...),
)
Passed via Web3WalletConfig.dialogTheme, consumed by WalletDialogService (lib/ethereum/wallet_dialog_service.dart:1).
Security #
- Private key: never log, never commit, store in
flutter_secure_storagewith biometrics, wipe on logout, consider external signer (WalletConnect) instead of embedding keys.EthereumProvider.initialize(lib/ethereum/ethereum_provider.dart:57) validates format;Web3WebViewshows error, not WebView, on failure.Clipboardnow usesflutter/serviceswithout logging (lib/utils/app_utils.dart:5). - XSS: all JS interpolation uses
jsonEncode(lib/provider/provider_script.dart:13,lib/web3_js_bridge_callback.dart:5,lib/ethereum/ethereum_provider.dart:817). - Origin: dialogs display
controller.getUrl().host; consider allowlist / phishing check beforeshowConnectWallet. - RPC:
rpcUrls.firstis used; use HTTPS + API key, pin certs viaonReceivedServerTrustAuthRequest. - Singleton:
EthereumProvideris a singleton; multipleWeb3WebViewshare state – avoid mounting two simultaneously, calldispose()(lib/ethereum/ethereum_provider.dart:245) on screen dispose (already done inWeb3WebView).
Limitations #
- Singleton
EthereumProvider– not multi-account, not multi-chain concurrent. ethers.min.js(464 KB) bundled atpackages/web3_webview/assets/ethers.min.js, injected twice.- No
eth_subscribe/eth_getLogspolling; unsupported methods throw4200. LoadingHelperis global ref-counted overlay (lib/utils/loading.dart:3).
Changelog #
See CHANGELOG.md. 1.0.9 fixes validations, XSS, permission default, non-blocking tx, EIP-712 padding, clipboard/log leaks, and removes clipboard/hex unused deps.
License & Thanks #
- License: see
LICENSE(BSD). - Thanks:
PositionExchange/flutter-web3-provider,ethers.jsv6 docs.