wallet_connect 1.0.1 wallet_connect: ^1.0.1 copied to clipboard
Wallet Connect client made in love with dart.
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wallet_connect/wallet_connect.dart';
import 'package:wallet_connect_example/eth_conversions.dart';
import 'package:wallet_connect_example/qr_scan_view.dart';
import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Wallet Connect',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Wallet Connect'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
const maticRpcUri =
'https://rpc-mainnet.maticvigil.com/v1/140d92ff81094f0f3d7babde06603390d7e581be';
enum MenuItems { PREVIOUS_SESSION, KILL_SESSION, SCAN_QR, CLEAR_CACHE }
class _MyHomePageState extends State<MyHomePage> {
late WCClient _wcClient;
late SharedPreferences _prefs;
late InAppWebViewController _webViewController;
late String walletAddress, privateKey;
bool connected = false;
WCSessionStore? _sessionStore;
final _web3client = Web3Client(
maticRpcUri,
http.Client(),
);
@override
void initState() {
_initialize();
super.initState();
}
_initialize() async {
_wcClient = WCClient(
onSessionRequest: _onSessionRequest,
onFailure: _onSessionError,
onDisconnect: _onSessionClosed,
onEthSign: _onSign,
onEthSignTransaction: _onSignTransaction,
onEthSendTransaction: _onSendTransaction,
onCustomRequest: (_, __) {},
onConnect: _onConnect,
);
// TODO: Mention walletAddress and privateKey while connecting
walletAddress = '';
privateKey = '';
_prefs = await SharedPreferences.getInstance();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: [
PopupMenuButton<MenuItems>(
onSelected: (item) {
switch (item) {
case MenuItems.PREVIOUS_SESSION:
_connectToPreviousSession();
break;
case MenuItems.KILL_SESSION:
_wcClient.killSession();
break;
case MenuItems.SCAN_QR:
Navigator.push(
context,
MaterialPageRoute(builder: (_) => QRScanView()),
).then((value) {
if (value != null) {
_qrScanHandler(value);
}
});
break;
case MenuItems.CLEAR_CACHE:
_webViewController.clearCache();
break;
}
},
itemBuilder: (_) {
return [
PopupMenuItem(
value: MenuItems.PREVIOUS_SESSION,
child: Text('Connect Previous Session'),
),
PopupMenuItem(
value: MenuItems.KILL_SESSION,
child: Text('Kill Session'),
),
PopupMenuItem(
value: MenuItems.SCAN_QR,
child: Text('Connect via QR'),
),
PopupMenuItem(
value: MenuItems.CLEAR_CACHE,
child: Text('Clear Cache'),
),
];
},
),
],
),
body: InAppWebView(
initialUrlRequest:
URLRequest(url: Uri.parse('https://example.walletconnect.org')),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
useShouldOverrideUrlLoading: true,
),
),
onWebViewCreated: (controller) {
_webViewController = controller;
},
shouldOverrideUrlLoading: (controller, navAction) async {
final uri = navAction.request.url;
final url = uri.toString();
debugPrint('URL $url');
if (url.startsWith('wc:')) {
if (url.contains('bridge') && url.contains('key')) {
_qrScanHandler(url);
}
return NavigationActionPolicy.CANCEL;
} else {
return NavigationActionPolicy.ALLOW;
}
},
),
);
}
_qrScanHandler(String value) {
final session = WCSession.from(value);
debugPrint('session $session');
final peerMeta = WCPeerMeta(
name: "Example Wallet",
url: "https://example.wallet",
description: "Example Wallet",
icons: [
"https://gblobscdn.gitbook.com/spaces%2F-LJJeCjcLrr53DcT1Ml7%2Favatar.png"
],
);
_wcClient.connectNewSession(session: session, peerMeta: peerMeta);
}
_connectToPreviousSession() {
final _sessionSaved = _prefs.getString('session');
debugPrint('_sessionSaved $_sessionSaved');
_sessionStore = _sessionSaved != null
? WCSessionStore.fromJson(jsonDecode(_sessionSaved))
: null;
if (_sessionStore != null) {
debugPrint('_sessionStore $_sessionStore');
_wcClient.connectFromSessionStore(_sessionStore!);
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('No previous session found.'),
));
}
}
_onConnect() {
setState(() {
connected = true;
});
}
_onSessionRequest(int id, WCPeerMeta peerMeta) {
showDialog(
context: context,
builder: (_) {
return SimpleDialog(
title: Column(
children: [
if (peerMeta.icons.isNotEmpty)
Container(
height: 100.0,
width: 100.0,
padding: const EdgeInsets.only(bottom: 8.0),
child: Image.network(peerMeta.icons.first),
),
Text(peerMeta.name),
],
),
contentPadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 16.0),
children: [
if (peerMeta.description.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(peerMeta.description),
),
if (peerMeta.url.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text('Connection to ${peerMeta.url}'),
),
Row(
children: [
Expanded(
child: TextButton(
style: TextButton.styleFrom(
primary: Colors.white,
backgroundColor: Theme.of(context).accentColor,
),
onPressed: () async {
_wcClient.approveSession(
accounts: [walletAddress],
// TODO: Mention Chain ID while connecting
chainId: 1,
);
_sessionStore = _wcClient.sessionStore;
await _prefs.setString('session',
jsonEncode(_wcClient.sessionStore.toJson()));
Navigator.pop(context);
},
child: Text('APPROVE'),
),
),
const SizedBox(width: 16.0),
Expanded(
child: TextButton(
style: TextButton.styleFrom(
primary: Colors.white,
backgroundColor: Theme.of(context).accentColor,
),
onPressed: () {
_wcClient.rejectSession();
Navigator.pop(context);
},
child: Text('REJECT'),
),
),
],
),
],
);
},
);
}
_onSessionError(dynamic message) {
setState(() {
connected = false;
});
showDialog(
context: context,
builder: (_) {
return SimpleDialog(
title: Text("Error"),
contentPadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 16.0),
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text('Some Error Occured. $message'),
),
Row(
children: [
TextButton(
style: TextButton.styleFrom(
primary: Colors.white,
backgroundColor: Theme.of(context).accentColor,
),
onPressed: () {
Navigator.pop(context);
},
child: Text('CLOSE'),
),
],
),
],
);
},
);
}
_onSessionClosed(int? code, String? reason) {
_prefs.remove('session');
setState(() {
connected = false;
});
showDialog(
context: context,
builder: (_) {
return SimpleDialog(
title: Text("Session Ended"),
contentPadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 16.0),
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text('Some Error Occured. ERROR CODE: $code'),
),
if (reason != null)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text('Failure Reason: $reason'),
),
Row(
children: [
TextButton(
style: TextButton.styleFrom(
primary: Colors.white,
backgroundColor: Theme.of(context).accentColor,
),
onPressed: () {
Navigator.pop(context);
},
child: Text('CLOSE'),
),
],
),
],
);
},
);
}
_onSignTransaction(
int id,
WCEthereumTransaction ethereumTransaction,
) {
_onTransaction(
id: id,
ethereumTransaction: ethereumTransaction,
title: 'Sign Transaction',
onConfirm: () async {
final creds = EthPrivateKey.fromHex(privateKey);
final tx = await _web3client.signTransaction(
creds,
_wcEthTxToWeb3Tx(ethereumTransaction),
chainId: _wcClient.chainId!,
);
// final txhash = await _web3client.sendRawTransaction(tx);
// debugPrint('txhash $txhash');
_wcClient.approveRequest<String>(
id: id,
result: bytesToHex(tx),
);
Navigator.pop(context);
},
onReject: () {
_wcClient.rejectRequest(id: id);
Navigator.pop(context);
},
);
}
_onSendTransaction(
int id,
WCEthereumTransaction ethereumTransaction,
) {
_onTransaction(
id: id,
ethereumTransaction: ethereumTransaction,
title: 'Send Transaction',
onConfirm: () async {
final creds = EthPrivateKey.fromHex(privateKey);
final txhash = await _web3client.sendTransaction(
creds,
_wcEthTxToWeb3Tx(ethereumTransaction),
chainId: _wcClient.chainId!,
);
debugPrint('txhash $txhash');
_wcClient.approveRequest<String>(
id: id,
result: txhash,
);
Navigator.pop(context);
},
onReject: () {
_wcClient.rejectRequest(id: id);
Navigator.pop(context);
},
);
}
_onTransaction({
required int id,
required WCEthereumTransaction ethereumTransaction,
required String title,
required VoidCallback onConfirm,
required VoidCallback onReject,
}) async {
ContractFunction? contractFunction;
BigInt gasPrice = BigInt.parse(ethereumTransaction.gasPrice ?? '0');
try {
final abiUrl =
'https://api.polygonscan.com/api?module=contract&action=getabi&address=${ethereumTransaction.to}&apikey=BCER1MXNFHP1TVE93CMNVKC5J4FV8R4CPR';
final res = await http.get(Uri.parse(abiUrl));
final Map<String, dynamic> resMap = jsonDecode(res.body);
final abi = ContractAbi.fromJson(resMap['result'], '');
final contract = DeployedContract(
abi, EthereumAddress.fromHex(ethereumTransaction.to));
final dataBytes = hexToBytes(ethereumTransaction.data);
final funcBytes = dataBytes.take(4).toList();
debugPrint("funcBytes $funcBytes");
final maibiFunctions = contract.functions
.where((element) => listEquals<int>(element.selector, funcBytes));
if (maibiFunctions.isNotEmpty) {
debugPrint("isNotEmpty");
contractFunction = maibiFunctions.first;
debugPrint("function ${contractFunction.name}");
// contractFunction.parameters.forEach((element) {
// debugPrint("params ${element.name} ${element.type.name}");
// });
// final params = dataBytes.sublist(4).toList();
// debugPrint("params $params ${params.length}");
}
if (gasPrice == BigInt.zero) {
gasPrice = await _web3client.estimateGas();
}
} catch (e, trace) {
debugPrint("failed to decode\n$e\n$trace");
}
showDialog(
context: context,
builder: (_) {
return SimpleDialog(
title: Column(
children: [
if (_wcClient.remotePeerMeta!.icons.isNotEmpty)
Container(
height: 100.0,
width: 100.0,
padding: const EdgeInsets.only(bottom: 8.0),
child: Image.network(_wcClient.remotePeerMeta!.icons.first),
),
Text(
_wcClient.remotePeerMeta!.name,
style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 20.0,
),
),
],
),
contentPadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 16.0),
children: [
Container(
alignment: Alignment.center,
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18.0,
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Receipient',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0,
),
),
const SizedBox(height: 8.0),
Text(
'${ethereumTransaction.to}',
style: TextStyle(fontSize: 16.0),
),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Expanded(
flex: 2,
child: Text(
'Transaction Fee',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0,
),
),
),
Expanded(
child: Text(
'${EthConversions.weiToEthUnTrimmed(gasPrice * BigInt.parse(ethereumTransaction.gas), 18)} MATIC',
style: TextStyle(fontSize: 16.0),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Expanded(
flex: 2,
child: Text(
'Transaction Amount',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0,
),
),
),
Expanded(
child: Text(
'${EthConversions.weiToEthUnTrimmed(BigInt.parse(ethereumTransaction.value ?? '0'), 18)} MATIC',
style: TextStyle(fontSize: 16.0),
),
),
],
),
),
if (contractFunction != null)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Function',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0,
),
),
const SizedBox(height: 8.0),
Text(
'${contractFunction.name}',
style: TextStyle(fontSize: 16.0),
),
],
),
),
Theme(
data:
Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: ExpansionTile(
tilePadding: EdgeInsets.zero,
title: Text(
'Data',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0,
),
),
children: [
Text(
'${ethereumTransaction.data}',
style: TextStyle(fontSize: 16.0),
),
],
),
),
),
Row(
children: [
Expanded(
child: TextButton(
style: TextButton.styleFrom(
primary: Colors.white,
backgroundColor: Theme.of(context).accentColor,
),
onPressed: onConfirm,
child: Text('CONFIRM'),
),
),
const SizedBox(width: 16.0),
Expanded(
child: TextButton(
style: TextButton.styleFrom(
primary: Colors.white,
backgroundColor: Theme.of(context).accentColor,
),
onPressed: onReject,
child: Text('REJECT'),
),
),
],
),
],
);
},
);
}
_onSign(
int id,
WCEthereumSignMessage ethereumSignMessage,
) {
final decoded = (ethereumSignMessage.type == WCSignType.TYPED_MESSAGE)
? ethereumSignMessage.data!
: ascii.decode(hexToBytes(ethereumSignMessage.data!));
showDialog(
context: context,
builder: (_) {
return SimpleDialog(
title: Column(
children: [
if (_wcClient.remotePeerMeta!.icons.isNotEmpty)
Container(
height: 100.0,
width: 100.0,
padding: const EdgeInsets.only(bottom: 8.0),
child: Image.network(_wcClient.remotePeerMeta!.icons.first),
),
Text(
_wcClient.remotePeerMeta!.name,
style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 20.0,
),
),
],
),
contentPadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 16.0),
children: [
Container(
alignment: Alignment.center,
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
'Sign Message',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18.0,
),
),
),
Theme(
data:
Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: ExpansionTile(
tilePadding: EdgeInsets.zero,
title: Text(
'Message',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0,
),
),
children: [
Text(
decoded,
style: TextStyle(fontSize: 16.0),
),
],
),
),
),
Row(
children: [
Expanded(
child: TextButton(
style: TextButton.styleFrom(
primary: Colors.white,
backgroundColor: Theme.of(context).accentColor,
),
onPressed: () async {
final creds = EthPrivateKey.fromHex(privateKey);
final encodedMessage =
(ethereumSignMessage.type == WCSignType.TYPED_MESSAGE)
? Uint8List.fromList(
utf8.encode(ethereumSignMessage.data!))
: hexToBytes(ethereumSignMessage.data!);
final signedData =
await creds.signPersonalMessage(encodedMessage);
final signedDataHex =
bytesToHex(signedData, include0x: true);
debugPrint('SIGNED $signedDataHex');
_wcClient.approveRequest<String>(
id: id,
result: signedDataHex,
);
Navigator.pop(context);
},
child: Text('SIGN'),
),
),
const SizedBox(width: 16.0),
Expanded(
child: TextButton(
style: TextButton.styleFrom(
primary: Colors.white,
backgroundColor: Theme.of(context).accentColor,
),
onPressed: () {
_wcClient.rejectRequest(id: id);
Navigator.pop(context);
},
child: Text('REJECT'),
),
),
],
),
],
);
},
);
}
Transaction _wcEthTxToWeb3Tx(WCEthereumTransaction ethereumTransaction) {
return Transaction(
from: EthereumAddress.fromHex(ethereumTransaction.from),
to: EthereumAddress.fromHex(ethereumTransaction.to),
maxGas: ethereumTransaction.gasLimit != null
? int.tryParse(ethereumTransaction.gasLimit!)
: null,
gasPrice: ethereumTransaction.gasPrice != null
? EtherAmount.inWei(BigInt.parse(ethereumTransaction.gasPrice!))
: null,
value: EtherAmount.inWei(BigInt.parse(ethereumTransaction.value ?? '0')),
data: hexToBytes(ethereumTransaction.data),
nonce: ethereumTransaction.nonce != null
? int.tryParse(ethereumTransaction.nonce!)
: null,
);
}
}