dapp_injected_eip155 1.0.1
dapp_injected_eip155: ^1.0.1 copied to clipboard
Support DApp Injected on Ethereum network - EIP155
example/lib/main.dart
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';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await ScriptUtils.initProviderScript();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
brightness: Brightness.dark,
),
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 = [
'0xB4A9cAB492C74A6e1C2503A3577b51d3856F8a8C',
'0x1bc9BDF4f77AD6662adD75628c6A65B4062Fd3f3',
];
final String _privateKey = ''; // mock
late NetworkSupport initNetwork = networkLi.first;
late String currentAddress = addressLi.first;
@override
Widget build(BuildContext context) {
return DAppInjectedView(
actions: [
_buildAccountSwitcher(context),
],
loadingChild: const Center(
child: SizedBox(
height: 32,
width: 32,
child: CircularProgressIndicator(),
),
),
title: const Text('DApp'),
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,
// ),
onShare: (url) {
/// add share action
debugPrint('SHARE: $url');
},
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,
),
);
}
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;
}
}