webviewEthInjected.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:flutter/cupertino.dart';
  4. import 'package:flutter/foundation.dart';
  5. import 'package:flutter/services.dart';
  6. import 'package:polkawallet_sdk/api/api.dart';
  7. import 'package:polkawallet_sdk/api/types/walletConnect/payloadData.dart';
  8. import 'package:polkawallet_sdk/consts/settings.dart';
  9. import 'package:polkawallet_sdk/storage/keyring.dart';
  10. import 'package:polkawallet_sdk/storage/keyringEVM.dart';
  11. import 'package:polkawallet_sdk/storage/types/keyPairData.dart';
  12. import 'package:polkawallet_sdk/webviewWithExtension/types/signExtrinsicParam.dart';
  13. import 'package:url_launcher/url_launcher.dart';
  14. import 'package:webview_flutter/webview_flutter.dart';
  15. class WebViewEthInjected extends StatefulWidget {
  16. WebViewEthInjected(
  17. this.api,
  18. this.initialUrl,
  19. this.keyringEVM, {
  20. required this.keyring,
  21. required this.onSwitchEvmChain,
  22. required this.onEvmRpcCall,
  23. required this.onAccountEmpty,
  24. this.onPageFinished,
  25. this.onExtensionReady,
  26. this.onWebViewCreated,
  27. this.onSignRequestEVM,
  28. this.onSignRequest,
  29. this.onConnectRequest,
  30. this.checkAuth,
  31. });
  32. final String initialUrl;
  33. final PolkawalletApi api;
  34. final KeyringEVM keyringEVM;
  35. final Keyring keyring;
  36. final Function(String)? onPageFinished;
  37. final Function? onExtensionReady;
  38. final Function(WebViewController)? onWebViewCreated;
  39. /// onSignRequestEVM handles EVM requests
  40. final Future<WCCallRequestResult?> Function(Map)? onSignRequestEVM;
  41. /// onSignBytesRequest & onSignExtrinsicRequest handles Substrate requests
  42. final Future<ExtensionSignResult?> Function(SignAsExtensionParam)?
  43. onSignRequest;
  44. final Future<List<KeyPairData>> Function(DAppConnectParam, {bool isEvm})?
  45. onConnectRequest;
  46. final List<KeyPairData> Function(String, {bool isEvm})? checkAuth;
  47. final Future<bool> Function(String) onSwitchEvmChain;
  48. final Future<Map> Function(Map) onEvmRpcCall;
  49. final Future<void> Function(String) onAccountEmpty;
  50. @override
  51. _WebViewEthInjectedState createState() => _WebViewEthInjectedState();
  52. }
  53. class _WebViewEthInjectedState extends State<WebViewEthInjected> {
  54. late WebViewController _controller;
  55. bool _loadingFinished = false;
  56. bool _signing = false;
  57. Future<dynamic> _respondToDApp(Map msg, Map? res) async {
  58. print('respond ${msg['name']} to dapp:');
  59. print(res);
  60. return _controller.runJavaScript(
  61. 'msgFromPolkawallet({name: "${msg['name']}", data: ${res != null ? jsonEncode(res) : null}})');
  62. }
  63. Future<dynamic> _msgHandler(Map msg) async {
  64. if (msg['msgType'] != null) {
  65. return _msgHandlerSubstrate(msg);
  66. }
  67. final res = {...(msg['data'] as Map)};
  68. res.remove('toNative');
  69. final uri = Uri.parse(msg['origin']);
  70. final method = res['method'];
  71. final isSigningMethod = SigningMethodsEVM.contains(method);
  72. if (!isSigningMethod) {
  73. final data = await widget.onEvmRpcCall(res);
  74. res['result'] = data['result'];
  75. return _respondToDApp(msg, res);
  76. }
  77. final authed = widget.checkAuth!(uri.host, isEvm: true);
  78. if (method != 'eth_requestAccounts' &&
  79. method != 'eth_accounts' &&
  80. authed.isEmpty) {
  81. res['error'] = ['unauthorized', 'wallet accounts unauthorized.'];
  82. return _respondToDApp(msg, res);
  83. }
  84. switch (method) {
  85. case 'eth_requestAccounts':
  86. case 'eth_accounts':
  87. if (_signing) break;
  88. _signing = true;
  89. List<KeyPairData> accountsAuthed = [];
  90. if (widget.keyringEVM.keyPairs.isEmpty) {
  91. await widget.onAccountEmpty('evm');
  92. _signing = false;
  93. } else if (authed.isNotEmpty) {
  94. res['result'] = authed.map((e) => e.address).toList();
  95. _signing = false;
  96. return _respondToDApp(msg, res);
  97. } else {
  98. accountsAuthed = await widget.onConnectRequest!(
  99. DAppConnectParam.fromJson(
  100. {'id': res['id'].toString(), 'url': msg['origin']}),
  101. isEvm: true);
  102. _signing = false;
  103. }
  104. if (accountsAuthed.isNotEmpty) {
  105. res['result'] = accountsAuthed.map((e) => e.address).toList();
  106. return _respondToDApp(msg, res);
  107. }
  108. res['error'] = [
  109. 'userRejectedRequest',
  110. 'User denied account authorization.'
  111. ];
  112. return _respondToDApp(msg, res);
  113. case 'wallet_switchEthereumChain':
  114. bool? accept = false;
  115. if (widget.keyringEVM.keyPairs.isEmpty) {
  116. await widget.onAccountEmpty('evm');
  117. } else {
  118. if (_signing) break;
  119. _signing = true;
  120. accept = await widget.onSwitchEvmChain(res['params'][0]['chainId']);
  121. _signing = false;
  122. }
  123. if (accept != true) {
  124. res['error'] = ['userRejectedRequest', 'User denied network switch.'];
  125. }
  126. res['result'] = null;
  127. return _respondToDApp(msg, res);
  128. case 'metamask_getProviderState':
  129. final chainId = int.parse(widget.api.connectedNode?.chainId ?? '1');
  130. res['result'] = {
  131. 'accounts': [widget.keyringEVM.current.address],
  132. 'chainId': '0x${chainId.toRadixString(16)}',
  133. 'isUnlocked': true,
  134. 'networkVersion': '0',
  135. };
  136. return _respondToDApp(msg, res);
  137. case 'eth_sign':
  138. case 'personal_sign':
  139. case 'eth_signTypedData':
  140. case 'eth_signTypedData_v4':
  141. case 'eth_signTransaction':
  142. case 'eth_sendTransaction':
  143. if (_signing) break;
  144. _signing = true;
  145. final signed = await widget.onSignRequestEVM!(msg);
  146. _signing = false;
  147. if (signed == null) {
  148. // cancelled
  149. res['error'] = ['userRejectedRequest', 'User rejected sign request.'];
  150. } else if (signed.result != null) {
  151. res['result'] = signed.result;
  152. } else {
  153. res['error'] = ['userRejectedRequest', signed.error];
  154. }
  155. return _respondToDApp(msg, res);
  156. default:
  157. print('Unknown message from dapp: ${msg['msgType']}');
  158. res['error'] = ['unauthorized', 'Method $method not support.'];
  159. return _respondToDApp(msg, res);
  160. }
  161. return Future(() => "");
  162. }
  163. Future<dynamic> _msgHandlerSubstrate(Map msg) async {
  164. final uri = Uri.parse(msg['url']);
  165. final authed = widget.checkAuth!(uri.host);
  166. if (msg['msgType'] != 'pub(authorize.tab)' &&
  167. widget.checkAuth != null &&
  168. authed.isEmpty) {
  169. return _controller.runJavaScript(
  170. 'walletExtension.onAppResponse("${msg['msgType']}${msg['id']}", null, new Error("Rejected"))');
  171. }
  172. switch (msg['msgType']) {
  173. case 'pub(authorize.tab)':
  174. if (widget.onConnectRequest == null) {
  175. return _controller.runJavaScript(
  176. 'walletExtension.onAppResponse("${msg['msgType']}${msg['id']}", true)');
  177. }
  178. if (widget.keyring.keyPairs.isEmpty) {
  179. await widget.onAccountEmpty('substrate');
  180. return _controller.runJavaScript(
  181. 'walletExtension.onAppResponse("${msg['msgType']}${msg['id']}", false, null)');
  182. }
  183. if (_signing) break;
  184. _signing = true;
  185. final addressAuthed = authed.isNotEmpty
  186. ? authed
  187. : await widget.onConnectRequest!(DAppConnectParam.fromJson(
  188. {'id': msg['id'], 'url': msg['url']}));
  189. _signing = false;
  190. return _controller.runJavaScript(
  191. 'walletExtension.onAppResponse("${msg['msgType']}${msg['id']}", ${addressAuthed.isNotEmpty ?? false}, null)');
  192. case 'pub(accounts.list)':
  193. case 'pub(accounts.subscribe)':
  194. final List res = authed.map((e) {
  195. return {
  196. 'address': e.address,
  197. 'name': e.name,
  198. 'genesisHash': '',
  199. };
  200. }).toList();
  201. return _controller.runJavaScript(
  202. 'walletExtension.onAppResponse("${msg['msgType']}${msg['id']}", ${jsonEncode(res)})');
  203. case 'pub(bytes.sign)':
  204. if (_signing) break;
  205. _signing = true;
  206. final SignAsExtensionParam param =
  207. SignAsExtensionParam.fromJson(msg as Map<String, dynamic>);
  208. final res = await widget.onSignRequest!(param);
  209. _signing = false;
  210. if (res == null || res.signature == null) {
  211. // cancelled
  212. return _controller.runJavaScript(
  213. 'walletExtension.onAppResponse("${param.msgType}${msg['id']}", null, new Error("Rejected"))');
  214. }
  215. return _controller.runJavaScript(
  216. 'walletExtension.onAppResponse("${param.msgType}${msg['id']}", ${jsonEncode(res.toJson())})');
  217. case 'pub(extrinsic.sign)':
  218. if (_signing) break;
  219. _signing = true;
  220. final SignAsExtensionParam params =
  221. SignAsExtensionParam.fromJson(msg as Map<String, dynamic>);
  222. final result = await widget.onSignRequest!(params);
  223. _signing = false;
  224. if (result == null || result.signature == null) {
  225. // cancelled
  226. return _controller.runJavaScript(
  227. 'walletExtension.onAppResponse("${params.msgType}${msg['id']}", null, new Error("Rejected"))');
  228. }
  229. return _controller.runJavaScript(
  230. 'walletExtension.onAppResponse("${params.msgType}${msg['id']}", ${jsonEncode(result.toJson())})');
  231. default:
  232. print('Unknown message from dapp: ${msg['msgType']}');
  233. return Future(() => "");
  234. }
  235. return Future(() => "");
  236. }
  237. Future<void> _onFinishLoad(String url) async {
  238. // if (_loadingFinished) return;
  239. // setState(() {
  240. // _loadingFinished = true;
  241. // });
  242. if (widget.onPageFinished != null) {
  243. widget.onPageFinished!(url);
  244. }
  245. print('Page loaded: $url');
  246. print('Inject EVM dapp js code...');
  247. final jsCodeEVM = await rootBundle.loadString(
  248. 'packages/polkawallet_sdk/js_as_extension/dist/ethereum.js');
  249. await _controller.runJavaScript(jsCodeEVM);
  250. print('EVM js code injected');
  251. print('Inject Substrate dapp js code...');
  252. final jsCode = await rootBundle
  253. .loadString('packages/polkawallet_sdk/js_as_extension/dist/main.js');
  254. await _controller.runJavaScript(jsCode);
  255. print('Substrate js code injected');
  256. if (widget.onExtensionReady != null) {
  257. widget.onExtensionReady!();
  258. }
  259. }
  260. Future<void> _launchWalletConnectLink(Uri url) async {
  261. if (await canLaunchUrl(url)) {
  262. try {
  263. await launchUrl(url, mode: LaunchMode.externalApplication);
  264. } catch (err) {
  265. if (kDebugMode) {
  266. print(err);
  267. }
  268. }
  269. } else {
  270. debugPrint('Could not launch $url');
  271. }
  272. }
  273. @override
  274. void initState() {
  275. super.initState();
  276. final WebViewController controller =
  277. WebViewController.fromPlatformCreationParams(
  278. const PlatformWebViewControllerCreationParams());
  279. controller
  280. ..setJavaScriptMode(JavaScriptMode.unrestricted)
  281. ..setNavigationDelegate(
  282. NavigationDelegate(
  283. onPageFinished: (String url) {
  284. _onFinishLoad(url);
  285. },
  286. onNavigationRequest: (NavigationRequest request) {
  287. if (request.url.startsWith('wc:')) {
  288. _launchWalletConnectLink(Uri.parse(request.url));
  289. return NavigationDecision.prevent;
  290. }
  291. return NavigationDecision.navigate;
  292. },
  293. ),
  294. )
  295. ..addJavaScriptChannel(
  296. 'Extension',
  297. onMessageReceived: (JavaScriptMessage message) {
  298. print('msg from dapp: ${message.message}');
  299. final msg = jsonDecode(message.message);
  300. if (msg['path'] != 'extensionRequest') return;
  301. _msgHandler(msg['data']);
  302. },
  303. )
  304. ..loadRequest(Uri.parse(widget.initialUrl));
  305. _controller = controller;
  306. }
  307. @override
  308. Widget build(BuildContext context) {
  309. return WebViewWidget(controller: _controller);
  310. }
  311. }