dapp_injected_eip155 1.0.5
dapp_injected_eip155: ^1.0.5 copied to clipboard
Support to inject the provider API into websites visited by its users using the window.ethereum provider object
example/lib/main.dart
import 'dart:async';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:flutter/material.dart';
import 'package:web3dart/crypto.dart';
import 'package:dapp_injected_eip155/dapp_injected_eip155.dart';
import 'package:web3dart/json_rpc.dart';
import 'js_transaction.dart';
import 'package:web3dart/web3dart.dart';
import 'package:http/http.dart' as http;
import 'network_support.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'widgets/progress_painter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await ScriptUtils.initProviderScript();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
appBarTheme: const AppBarTheme(backgroundColor: Colors.white),
scaffoldBackgroundColor: Colors.white,
brightness: Brightness.light,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final TextEditingController _urlController =
TextEditingController(text: 'https://pancakeswap.finance/');
@override
void dispose() {
_urlController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _urlController,
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (c) => ExampleDapp(
initialUrl: _urlController.text.trim(),
)),
);
},
child: const Text('DApp Injected'),
),
],
),
),
);
}
}
class ExampleDapp extends StatefulWidget {
final String initialUrl;
const ExampleDapp({super.key, required this.initialUrl});
@override
State<ExampleDapp> createState() => _ExampleDappState();
}
class _ExampleDappState extends State<ExampleDapp> {
final List<NetworkSupport> networkLi = [
NetworkSupport(
chainId: 1,
chainName: 'Ethereum',
nativeCurrency: NativeCurrency(
name: 'ETH',
symbol: 'eth',
decimals: 18,
),
rpcUrls: [
"https://rpc.ankr.com/eth",
],
blockExplorerUrls: [
"https://etherscan.io/tx/",
],
),
];
final List<String> addressLi = [
'0xDBEC6913a697B61F218B4a5D33B7561800Fe04E9',
'0x1bc9BDF4f77AD6662adD75628c6A65B4062Fd3f3',
];
final String _privateKey = ''; // mock
late NetworkSupport initNetwork = networkLi.first;
late String currentAddress = addressLi.first;
final StreamController<double> _progressController =
StreamController<double>();
InAppWebViewController? _webViewController;
@override
void dispose() {
_progressController.sink.close();
_progressController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppBar(),
body: SafeArea(
top: false,
child: _buildBody(context),
),
);
}
AppBar _buildAppBar() {
return AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded),
onPressed: () async {
bool canBack = (await _webViewController?.canGoBack()) ?? false;
if (canBack) {
await _webViewController?.goBack();
return;
}
if (mounted) {
Navigator.of(context).pop();
}
},
),
title: const Text('DApp Browser'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(2),
child: StreamBuilder<double>(
initialData: 0,
stream: _progressController.stream,
builder: (c, asyncSnapshot) {
if (asyncSnapshot.data == 1) {
return const SizedBox();
}
return ProgressPainter(
progress: asyncSnapshot.data!,
baseColor: Theme.of(context).appBarTheme.backgroundColor,
primaryColor: Colors.blueAccent,
);
},
),
),
actions: [
_buildAccountSwitcher(context),
],
);
}
Widget _buildBody(BuildContext context) {
return DAppInjectedView(
loadingChild: const Center(
child: SizedBox(
height: 32,
width: 32,
child: CircularProgressIndicator(),
),
),
initialScript: '''
window.ethereum.isMetaMask = true;
''',
/// set isMetaMask flag
initialUrl: widget.initialUrl,
currentProvider: WalletWeb3Provider(
address: currentAddress,
chainId: initNetwork.chainId,
rpcUrl: initNetwork.rpcUrl,
),
initialSettings: InAppWebViewSettings(
isInspectable: true, //kDebugMode,
mediaPlaybackRequiresUserGesture: true,
allowsInlineMediaPlayback: true,
iframeAllowFullscreen: true,
javaScriptEnabled: true,
supportMultipleWindows: false,
/// warning if set 'true'
),
methodCallbacks: MethodCallbacks(
onSignTransaction: _onSignTransaction,
onSwitchNetwork: (chainId) async {
/// need check case chainId not support
int index = networkLi.indexWhere((e) => e.chainId == chainId);
if (index == -1) {
throw const NotFoundChainException();
}
// currentNetwork = networkLi[index];
return networkLi[index].rpcUrl;
},
onAddNetwork: (data) async {
///user reject
// throw const UserRejectException();
NetworkSupport network = NetworkSupport.fromJson(data);
networkLi.add(network);
return (chainId: network.chainId!, rpcUrl: network.rpcUrl);
},
onSignMessage: _onSignMessage,
onSignPersonalMessage: _onSignPersonalMessage,
onSignTypeMessage: _onSignTypeMessage,
// onEcRecover: _onEcRecover,
),
onWebViewCreated: (controller) {
_webViewController = controller;
},
onWebViewClosed: () {
_webViewController = null;
},
shouldOverrideUrlLoading: (c, navAction) async {
// final url = navAction.request.url.toString();
return NavigationActionPolicy.ALLOW;
},
onProgressChanged: (controller, progress) {
_progressController.sink.add(progress / 100);
},
onReceivedError: (controller, request, error) {
_progressController.sink.add(1);
},
);
}
Widget _buildAccountSwitcher(BuildContext context) {
return IconButton(
onPressed: () async {
await showModalBottomSheet(
context: context,
builder: (context) {
return ListView.builder(
padding: const EdgeInsets.only(top: 25, bottom: 35),
itemBuilder: (c, index) {
bool isActive = currentAddress == addressLi[index];
return GestureDetector(
onTap: () {
Navigator.of(context).pop();
if (isActive) return;
currentAddress = addressLi[index];
setState(() {});
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 16),
child: Text(
addressLi[index],
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: isActive
? Theme.of(context).primaryColor
: null,
),
),
),
);
},
itemCount: addressLi.length,
);
});
},
icon: const Icon(Icons.menu),
);
}
Future<bool> _confirmSignMessage(String title, String message) async {
bool? status = await showModalBottomSheet(
context: context,
builder: (c) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 16),
child: Column(
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineLarge,
),
const SizedBox(height: 25),
Expanded(
child: SingleChildScrollView(
child: Text(
message,
style: Theme.of(context).textTheme.bodyLarge,
),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () async {
Navigator.of(context).pop(false);
},
child: const Text('Cancel'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () async {
Navigator.of(context).pop(true);
},
child: const Text('Confirm'),
),
),
],
)
],
),
);
},
);
return status ?? false;
}
Future<String> _onSignTransaction(
int? chainId, String? rpcUrl, Map<String, dynamic> data) async {
if (chainId == null || rpcUrl == null) {
throw const NotFoundChainException();
}
/// if user reject
// throw const UserRejectException();
JsTransaction transaction = JsTransaction.fromJson(data);
try {
final tx = Transaction(
from: EthereumAddress.fromHex(transaction.from),
data: hexToBytes(transaction.data),
value: EtherAmount.fromBase10String(
EtherUnit.wei, transaction.value ?? '0'),
to: EthereumAddress.fromHex(transaction.to),
maxGas: 61931,
/// mock or transaction.gas
);
final client = Web3Client(rpcUrl, http.Client());
final hash = await client.sendTransaction(
EthPrivateKey.fromHex(_privateKey),
tx,
chainId: chainId,
);
return hash;
} on RPCError catch (e) {
throw RpcException(e.errorCode, e.message);
} catch (e) {
rethrow;
}
}
Future<String> _onSignMessage(String type, String message) async {
bool isAccept = await _confirmSignMessage(type, message);
if (!isAccept) {
// user reject
throw const UserRejectException();
}
String signature = EthSigUtil.signMessage(
privateKey: _privateKey,
message: hexToBytes(message),
);
return signature;
}
Future<String> _onSignPersonalMessage(String type, String message) async {
bool isAccept = await _confirmSignMessage(type, message);
if (!isAccept) {
// user reject
throw const UserRejectException();
}
String signature = EthSigUtil.signPersonalMessage(
privateKey: _privateKey,
message: hexToBytes(message),
);
return signature;
}
Future<String> _onSignTypeMessage(String type, JsSignTypeData data) async {
bool isAccept = await _confirmSignMessage(type, data.raw);
if (!isAccept) {
// user reject
throw const UserRejectException();
}
TypedDataVersion? version;
switch (data.version) {
case 'V1':
version = TypedDataVersion.V1;
break;
case 'V3':
version = TypedDataVersion.V3;
break;
case 'V4':
version = TypedDataVersion.V4;
break;
default:
break;
}
if (version == null) {
throw const InvalidInputException();
}
final String signature = EthSigUtil.signTypedData(
privateKey: _privateKey,
jsonData: data.raw,
version: version,
);
return signature;
}
}