v1.0.9web3_webview
web3_webview는 WebView 내에서 실행 중인 DApp과 Flutter 애플리케이션 사이의 강력한 다리로, 안전하고 원활한 양방향 통신을 가능하게 합니다.
Web3 주입형 WebView 플러터
{"sdk":"flutter"}^6.1.5^1.3.0>=2.7.3 <3.0.0^3.0.6^3.0.0^0.20.2^0.2.0^4.5.1^1.0.2{"sdk":"flutter"}^6.0.0아래는 영문 원문 스냅샷입니다. 최신 내용은 GitHub에서 확인하세요.
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.
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 --> Signer[Web3Signer\nPrivateKeySigner | external]
Signer --> Signing[SigningHandler\nEIP-191 / EIP-712]
Signer --> 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), supports read-only when no signer, 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. New: Web3Signer? abstraction – PrivateKeySigner / external / null (read-only) |
Web3Signer |
lib/signer/web3_signer.dart |
Abstract address, signMessage(method,from,msg,pw), sendTransaction(tx); implement for WalletConnect / Secure Enclave etc. |
PrivateKeySigner |
lib/signer/private_key_signer.dart |
Local EthPrivateKey impl via SigningHandler + TransactionHandler, auto-updates on chain switch via getters |
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 |
eth_requestAccounts; returns [] on eth_accounts until connectedwindow.ethereum + window.web3.currentProvider, isMetaMask=true, EIP-6963 discoveryprivateKey/signer and DApp still loads; eth_call, eth_getBalance, eth_blockNumber etc. work, signing throws 4100Web3Signer for WalletConnect, Secure Enclave, biometrics – no private key in RAMeth_estimateGas buffered, non-blocking hash return, fire-and-forget receipt polling (opt-in blocking)personal_sign (EIP-191 prefix), eth_sign (raw keccak), eth_signTypedData/v1/v3/v4 (EIP-712)wallet_switchEthereumChain/wallet_addEthereumChain with idempotency checksflutter_inappwebview callHandler ↔ evaluateJavascript, XSS-safe via jsonEncodeNetworkConfig>=3.24.0, Dart ^3.5.0flutter_inappwebview ^6.1.5, web3dart ^2.7.3dependencies:
web3_webview: ^1.0.11
import 'package:web3_webview/web3_webview.dart';
privateKey is now optional. If omitted and no signer is provided, the WebView runs in read-only mode (only RPC reads succeed; signing throws 4100). signer takes precedence over privateKey.
Do not hardcode keys. Load from
flutter_secure_storage/ Keychain / Keystore and clear on lock. Or useWeb3Signerto avoid storing keys at all. See Security.
Web3WebView(
web3WalletConfig: Web3WalletConfig(
currentNetwork: ethMainnet,
supportNetworks: [ethMainnet, bscMainnet],
name: 'MyDApp',
),
initialUrlRequest: URLRequest(url: WebUri('https://metamask.github.io/test-dapp/')),
)
// DApp loads, eth_call/eth_getBalance work, eth_sendTransaction -> {code:4100}
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
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!, // 0x + 64 hex, validated -32602 on invalid
currentNetwork: ethMainnet,
supportNetworks: [ethMainnet, bscMainnet],
name: 'MyDApp Wallet',
id: 'com.example.mydapp',
dialogTheme: WalletDialogTheme(primaryColor: Color(0xFF3B82F6)),
onError: (method, params, message) => debugPrint('[$method] $message'),
),
initialUrlRequest: URLRequest(url: WebUri('https://metamask.github.io/test-dapp/')),
onPermissionRequest: (ctrl, req) async => PermissionResponse(
resources: req.resources, action: PermissionResponseAction.DENY,
),
);
}
}
import 'package:web3_webview/web3_webview.dart';
class WalletConnectSigner extends Web3Signer {
final String _addr;
WalletConnectSigner(this._addr);
@override String get address => _addr;
@override Future<String> signMessage(String method, String from, dynamic message, String pw) {
// delegate to WalletConnect / enclave
return walletConnect.sign(method, from, message);
}
@override Future<String> sendTransaction(Map<String, dynamic> tx) {
return walletConnect.sendTransaction(tx);
}
}
Web3WebView(
web3WalletConfig: Web3WalletConfig(
signer: WalletConnectSigner('0xAbc...'),
currentNetwork: ethMainnet,
),
initialUrlRequest: URLRequest(url: WebUri('https://app.uniswap.org/')),
)
Web3WalletConfig| Field | Type | Required | Notes |
|---|---|---|---|
privateKey |
String? |
no* | 0x + 64 hex or 64 hex; validated with -32602 on invalid. *Required only for local signing; omit for read-only / external signer |
signer |
Web3Signer? |
no* | external signer (WalletConnect etc.), takes precedence over privateKey. If both null → read-only |
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? }
Read-only check: config.isReadOnly / config.hasSigner; provider: EthereumProvider().isReadOnly.
lib/signer/web3_signer.dart:14:
abstract class Web3Signer {
String get address; // EIP-55
Future<String> signMessage(String method, String from, dynamic message, String password);
Future<String> sendTransaction(Map<String, dynamic> txParams); // -> tx hash
Future<Uint8List> signTransaction(Transaction tx, int chainId) => throw UnsupportedError(...);
}
Web3WalletConfig() with no privateKey/signer → EthereumProvider.isReadOnly==true, window.ethereum.isConnected==false, eth_accounts==[]. eth_call, eth_getBalance, eth_blockNumber, eth_chainId, eth_estimateGas, wallet_switchEthereumChain etc. still work via Web3Client. Signing methods throw 4100 (unauthorized) which surfaces as code:4100 in DApp.PrivateKeySigner lib/signer/private_key_signer.dart:11 is auto-created from privateKey; uses SigningHandler/TransactionHandler internally and auto-updates chainId/Web3Client on network switch.Web3Signer and pass Web3WalletConfig(signer: ...). Dialogs (showConnectWallet/showSignMessage/showTransactionConfirm) are still shown by EthereumProvider before delegating; your signer only does crypto/RPC.EthereumProvider.handleRequest (lib/ethereum/ethereum_provider.dart:189) dispatches:
| Method | Params | Behaviour |
|---|---|---|
eth_requestAccounts |
[] |
Bottom sheet → connect + accountsChanged events, returns [address] or 4001; throws 4100 in read-only |
eth_accounts |
[] |
[] until connected (or read-only) |
eth_chainId |
[] |
state.chainId (hex) |
net_version |
[] |
decimal chainId |
eth_blockNumber |
[] |
12s cached HexUtils.numberToHex(blockNumber) |
eth_call |
[tx, block] |
makeRPCCall('eth_call') (no signing) |
eth_sendTransaction |
[tx] |
Dialog → signer.sendTransaction → hash immediately, polling fire-and-forget; 4100 in read-only |
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 (works in read-only) |
personal_sign |
[hexMsg, addr, pw?] |
dialog → signer.signMessage; 4100 in read-only |
eth_sign |
[addr, hexMsg] |
raw keccak256; 4100 in read-only |
eth_signTypedData[*_v1/v3/v4] |
[addr, typed] |
EIP-712; 4100 in read-only |
wallet_switchEthereumChain |
[{chainId}] |
idempotent, dialog if needed → chainChanged (works in read-only) |
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.
lib/exceptions.dart:2)WalletException.code follows EIP-1193 + JSON-RPC:
4001 – user rejected4100 – unauthorized (no signer / read-only)4200 – unsupported method4900 – disconnected-32601 – method not found-32602 – invalid params-32603 – internalWeb3WebView (lib/web3_webview_eip1193.dart:825) maps WalletException.code into the thrown {code,message} consumed by provider_script.dart:127 _processError.
onPermissionRequest defaults to DENY (lib/web3_webview_eip1193.dart:1019). Override Web3WebView.onPermissionRequest to grant selectively, never GRANT all.shouldInterceptRequest/onReceivedServerTrustAuthRequest are forwarded; implement cert pinning in host if needed.UserScript at AT_DOCUMENT_START; on Android re-evaluated in onLoadStart.lib/signing/signing_handler.dart:1)personal_sign accepts 0x hex or UTF-8, decodes for UI, signs prefixed.eth_sign signs keccak256(hexOrUtf8) directly (dangerous – dialog still shown).types must contain EIP712Domain; checks primaryType, domain whitelist (name,version,chainId,verifyingContract,salt), circular deps, array lengths, and ABI 32-byte padding for bool/address/bytesN/uint/int.lib/transaction/transaction_handler.dart:1)to (0x 42 chars), validates value/data as 0x hex.getTransactionCount(from) as nonce, estimateGas +20% with BigInt (*120/100), getGasPrice() or gasPrice override.handleTransaction({waitForConfirmation=false}) – default returns hash; set true to block up to 30×10s polling and throw on receipt.status==false or timeout.// Light (default)
final light = WalletDialogTheme(primaryColor: Color(0xFF6366F1));
// Dark – auto-used when Theme.brightness == dark, or pass custom
final dark = WalletDialogTheme.dark(primaryColor: Color(0xFF818CF8));
Web3WalletConfig(
dialogTheme: light,
darkDialogTheme: dark, // optional – if null auto-derived from light
)
WalletDialogService checks Theme.of(context).brightness – no extra code needed; BottomSheetDialog barrier 0.42, cards adapt (surface #1E293B, border #334155, text #F1F5F9).FilledButton 52h/14 radius + OutlinedButton, ExpansionTile hex.WalletDialogTheme(primaryColor:…, borderRadius:…), consumed by WalletDialogService (lib/ethereum/wallet_dialog_service.dart:7).Try the example: AppBar dark toggle → all eth_requestAccounts / personal_sign / eth_sendTransaction dialogs switch instantly.
You can keep the premium dialogs (theming only) or completely replace them to match your app branding – no fork needed.
// 1. Define builders once (or per-WebView)
final myBuilders = WalletDialogBuilders(
connect: (ctx, {required address, required host, required appName, required controller, required theme}) async {
return showDialog<bool>(
context: ctx,
barrierDismissible: false,
builder: (c) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text('$appName muốn kết nối', style: theme.headerStyle),
content: Text('Host: $host\nWallet: $address'),
actions: [
TextButton(onPressed: () => Navigator.pop(c, false), child: Text('Từ chối')),
FilledButton(onPressed: () => Navigator.pop(c, true), style: FilledButton.styleFrom(backgroundColor: theme.primaryColor), child: Text('Kết nối')),
],
),
);
},
// sign, transaction, switchNetwork, addNetwork – same pattern
sign: (ctx, {required message, required address, required host, required controller, required theme}) => ...,
transaction: (ctx, {required txParams, required host, required controller, required theme}) => ...,
);
// 2. Pass via config (per-WebView) – takes precedence
Web3WebView(
web3WalletConfig: Web3WalletConfig(
privateKey: pk,
dialogTheme: light,
darkDialogTheme: dark,
dialogBuilders: myBuilders, // <-- all dialogs now use your widgets
),
)
// 3. Or globally: WalletDialogService.instance.setBuilders(myBuilders);
BuildContext (for showDialog/Navigator), host, address/message/txParams, controller (for getUrl()), and the effective WalletDialogTheme (already resolved for light/dark + your custom colors).Future<bool?> – true = confirm, false/null = reject/cancel (maps to EIP-1193 4001).null builder falls back to the premium default; you can override only connect and keep sign premium.theme.primaryColor / textColor / borderColor to stay consistent with dialogTheme/darkDialogTheme.Example with 4 tabs (read-only / privateKey / signer / custom) is in example/lib/main.dart:56 – the “Custom” tab uses AlertDialog as above.
flutter_secure_storage with biometrics, wipe on logout. Prefer Web3Signer (WalletConnect / Secure Enclave) over embedding keys. EthereumProvider.initialize (lib/ethereum/ethereum_provider.dart:63) validates privateKey if provided; Web3WebView shows error, not WebView, on invalid format. Read-only mode needs no key. Clipboard uses flutter/services without logging (lib/utils/app_utils.dart:5).jsonEncode (lib/provider/provider_script.dart:13, lib/web3_js_bridge_callback.dart:5, lib/ethereum/ethereum_provider.dart:841).controller.getUrl().host; consider allowlist / phishing check before showConnectWallet.rpcUrls.first is used; use HTTPS + API key, pin certs via onReceivedServerTrustAuthRequest.EthereumProvider is a singleton; multiple Web3WebView share state – avoid mounting two simultaneously, call dispose() (lib/ethereum/ethereum_provider.dart:259) on screen dispose (already done in Web3WebView).EthereumProvider – not multi-account, not multi-chain concurrent.ethers.min.js (464 KB) bundled at packages/web3_webview/assets/ethers.min.js, injected twice.eth_subscribe/eth_getLogs polling; unsupported methods throw 4200.LoadingHelper is global ref-counted overlay (lib/utils/loading.dart:3).See CHANGELOG.md. 1.0.13 adds WalletDialogBuilders for fully custom UI (dialogBuilders), 1.0.12 premium + auto dark, 1.0.11 Web3Signer + read-only.
LICENSE (BSD).PositionExchange/flutter-web3-provider, ethers.js v6 docs.