From 2212b06cf65357b030114ce88a2cc7739a7a3c87 Mon Sep 17 00:00:00 2001 From: Francois Date: Mon, 2 Dec 2024 17:00:49 +0200 Subject: [PATCH] fix(kdf-wasm-ops): response type conversion and migrate to js_interop (#14) * migrate wasm ops to js_interop improve type conversion and error handling * remove runZoneGuarded from mm2Rpc function * migrate remaining files to js_interop --- .../app_build/build_config.json | 2 +- .../src/operations/kdf_operations_wasm.dart | 153 +++++++++++++----- .../web/res/kdf_wrapper.dart | 56 ++++--- .../kdf_operations_server_web.dart | 6 +- 4 files changed, 151 insertions(+), 66 deletions(-) diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index c9a35e0..cdcf22d 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -28,7 +28,7 @@ "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "5c8100dbefe16eb3eec9a672bb6e628ce37ddb57", + "bundled_coins_repo_commit": "958e6edd877e9a248eba0777af5ca3bd5196e2ea", "coins_repo_api_url": "https://api.github.com/repos/KomodoPlatform/coins", "coins_repo_content_url": "https://komodoplatform.github.io/coins", "coins_repo_branch": "master", diff --git a/packages/komodo_defi_framework/lib/src/operations/kdf_operations_wasm.dart b/packages/komodo_defi_framework/lib/src/operations/kdf_operations_wasm.dart index 7a20c55..b85f554 100644 --- a/packages/komodo_defi_framework/lib/src/operations/kdf_operations_wasm.dart +++ b/packages/komodo_defi_framework/lib/src/operations/kdf_operations_wasm.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:js_interop' as js_interop; import 'dart:js_interop_unsafe'; -import 'dart:js_util' as js_util; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -66,41 +65,29 @@ class KdfOperationsWasm implements IKdfOperations { (await kdfMainStatus()) == MainStatus.rpcIsUp; @override - // TODO! Ensure consistency accross implementations for behavior of kdMain - // and kdfStop wrt if the method is responsible only for initiating the - // operation or also for waiting for the operation to complete. - // Likely, it is the former, and then additional logic on top of this - // can be handled by [KomoDefiFramework] or the caller. Future kdfMain(JsonMap config, {int? logLevel}) async { await _ensureLoaded(); - // final startParams = await _configManager.generateStartParamsFromDefault( - // passphrase, - // userpass: _config.userpass, - // ); final mm2Config = { 'conf': config, 'log_level': logLevel ?? 3, }; - final jsConfig = js_util.jsify(mm2Config) as js_interop.JSObject; + final jsConfig = mm2Config.jsify() as js_interop.JSObject?; try { - final result = js_util.dartify( - _kdfModule!.callMethod( - 'mm2_main'.toJS, - jsConfig, - (int level, String message) { - _log('[$level] KDF: $message'); - }.toJS, - ), - ); + final result = _kdfModule! + .callMethod( + 'mm2_main'.toJS, + jsConfig, + (int level, String message) { + _log('[$level] KDF: $message'); + }.toJS, + ) + .dartify(); _log('mm2_main called: $result'); - // Similar logic to the local executable implementation: wait for kdf to - // start before returning, and assume failure instead of success if no - // response is received from the isRunning function. final timer = Stopwatch()..start(); while (timer.elapsed.inSeconds < 15) { if (await isRunning()) { @@ -139,9 +126,9 @@ class KdfOperationsWasm implements IKdfOperations { await _ensureLoaded(); try { - final errorOrNull = - await (js_util.dartify(_kdfModule!.callMethod('mm2_stop'.toJS))! - as Future); + final errorOrNull = await (_kdfModule! + .callMethod('mm2_stop'.toJS) + .dartify()! as Future); if (errorOrNull is int) { return StopStatus.fromDefaultInt(errorOrNull); @@ -149,7 +136,6 @@ class KdfOperationsWasm implements IKdfOperations { _log('KDF stop result: $errorOrNull'); - // Wait until the KDF is stopped. Timeout after 10 seconds await Future.doWhile(() async { final isStopped = (await kdfMainStatus()) == MainStatus.notRunning; @@ -173,33 +159,112 @@ class KdfOperationsWasm implements IKdfOperations { @override Future mm2Rpc(JsonMap request) async { - try { - await _ensureLoaded(); + await _ensureLoaded(); + + final jsResponse = await _makeJsCall(request); + final dartResponse = _parseDartResponse(jsResponse, request); + _validateResponse(dartResponse, request, jsResponse); + + return JsonMap.from(dartResponse); + } + + /// Makes the JavaScript RPC call and returns the raw JS response + Future _makeJsCall(JsonMap request) async { + if (kDebugMode) _log('mm2Rpc request: $request'); + request['userpass'] = _config.rpcPassword; - if (kDebugMode) _log('mm2Rpc request (pre-process): $request'); - request['userpass'] = _config.rpcPassword; + final jsRequest = request.jsify() as js_interop.JSObject?; + final jsPromise = _kdfModule!.callMethod('mm2_rpc'.toJS, jsRequest) + as js_interop.JSPromise?; - final jsResponse = await js_util.promiseToFuture( - _kdfModule!.callMethod( - 'mm2_rpc'.toJS, - js_util.jsify(request) as js_interop.JSObject, - ), + if (jsPromise == null || jsPromise.isUndefinedOrNull) { + throw Exception( + 'mm2_rpc call returned null for method: ${request['method']}' + '\nRequest: $request', ); + } - if (kDebugMode) _log('Response pre-cast: ${js_util.dartify(jsResponse)}'); + final jsResponse = await jsPromise.toDart + .then((value) => value) + .catchError((Object error) { + if (error.toString().contains('RethrownDartError')) { + final errorMessage = error.toString().split('\n')[0]; + throw Exception( + 'JavaScript error for method ${request['method']}: $errorMessage' + '\nRequest: $request', + ); + } + throw Exception( + 'Unknown error for method ${request['method']}: $error' + '\nRequest: $request', + ); + }); + + if (jsResponse == null || jsResponse.isUndefinedOrNull) { + throw Exception( + 'mm2_rpc response was null for method: ${request['method']}' + '\nRequest: $request', + ); + } - // Convert the JS object to a Dart map and ensure it's a JsonMap - final response = - JsonMap.from((jsResponse.dartify()! as Map).cast()); + if (kDebugMode) _log('Raw JS response: $jsResponse'); + return jsResponse as js_interop.JSObject; + } - return response; + /// Converts JS response to Dart Map + JsonMap _parseDartResponse( + js_interop.JSObject jsResponse, + JsonMap request, + ) { + try { + final dynamic converted = jsResponse.dartify(); + if (converted is! JsonMap) { + return _deepConvertMap(converted as Map); + } + return converted; } catch (e) { - final message = 'Error calling mm2Rpc: $e. ${request['method']}'; - _log(message); - throw Exception(message); + _log('Response parsing error for method ${request['method']}:\n' + 'Request: $request'); + rethrow; } } + /// Validates the response structure + void _validateResponse( + JsonMap dartResponse, + JsonMap request, + js_interop.JSObject jsResponse, + ) { + if (!dartResponse.containsKey('result') && + !dartResponse.containsKey('error')) { + throw Exception( + 'Invalid response format for method ${request['method']}\nResponse: ' + '$dartResponse\nRaw JS Response: $jsResponse\nRequest: $request', + ); + } + } + + /// Recursively converts the provided map to JsonMap. This is required, as + /// many of the responses received from the sdk are + /// LinkedHashMap + Map _deepConvertMap(Map map) { + return map.map((key, value) { + if (value is Map) return MapEntry(key.toString(), _deepConvertMap(value)); + if (value is List) { + return MapEntry(key.toString(), _deepConvertList(value)); + } + return MapEntry(key.toString(), value); + }); + } + + List _deepConvertList(List list) { + return list.map((value) { + if (value is Map) return _deepConvertMap(value); + if (value is List) return _deepConvertList(value); + return value; + }).toList(); + } + @override Future validateSetup() async { await _ensureLoaded(); diff --git a/packages/komodo_defi_framework/web/res/kdf_wrapper.dart b/packages/komodo_defi_framework/web/res/kdf_wrapper.dart index 1acf13e..b6793c4 100644 --- a/packages/komodo_defi_framework/web/res/kdf_wrapper.dart +++ b/packages/komodo_defi_framework/web/res/kdf_wrapper.dart @@ -3,11 +3,12 @@ import 'dart:async'; // This is a web-specific file, so it's safe to ignore this warning // ignore: avoid_web_libraries_in_flutter -import 'dart:js' as js; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; -import 'package:js/js_util.dart'; +import 'package:web/web.dart'; class KdfPlugin { static void registerWith(Registrar registrar) { @@ -57,38 +58,57 @@ class KdfPlugin { final completer = Completer(); - final script = - js.context['document'].callMethod('createElement', ['script']); - script['src'] = 'kdf/kdflib.js'; - script['onload'] = js.allowInterop(() { - _libraryLoaded = true; - completer.complete(); - }); - script['onerror'] = js.allowInterop((event) { - completer.completeError('Failed to load kdflib.js'); - }); + final script = (document.createElement('script') as HTMLScriptElement) + ..src = 'kdf/kdflib.js' + ..onload = () { + _libraryLoaded = true; + completer.complete(); + }.toJS + ..onerror = (event) { + completer.completeError('Failed to load kdflib.js'); + }.toJS; - js.context['document']['head'].callMethod('appendChild', [script]); + document.head!.appendChild(script); return completer.future; } Future _mm2Main(String conf, Function logCallback) async { await _ensureLoaded(); - return dartify( - js.context.callMethod('mm2_main', [conf, js.allowInterop(logCallback)]), - )! as int; + + try { + final jsCallback = logCallback.toJS; + final jsResponse = globalContext.callMethod( + 'mm2_main'.toJS, + [conf.toJS, jsCallback].toJS, + ); + if (jsResponse == null) { + throw Exception('mm2_main call returned null'); + } + + final dynamic dartResponse = (jsResponse as JSAny?).dartify(); + if (dartResponse == null) { + throw Exception('Failed to convert mm2_main response to Dart'); + } + + return dartResponse as int; + } catch (e) { + throw Exception('Error in mm2_main: $e\nConfig: $conf'); + } } int _mm2MainStatus() { if (!_libraryLoaded) { throw StateError('KDF library not loaded. Call ensureLoaded() first.'); } - return js.context.callMethod('mm2_main_status') as int; + + final jsResult = globalContext.callMethod('mm2_main_status'.toJS); + return jsResult.dartify()! as int; } Future _mm2Stop() async { await _ensureLoaded(); - return js.context.callMethod('mm2_stop') as int; + final jsResult = globalContext.callMethod('mm2_stop'.toJS); + return jsResult.dartify()! as int; } } diff --git a/playground/lib/kdf_operations/kdf_operations_server_web.dart b/playground/lib/kdf_operations/kdf_operations_server_web.dart index 5c3767c..0c90222 100644 --- a/playground/lib/kdf_operations/kdf_operations_server_web.dart +++ b/playground/lib/kdf_operations/kdf_operations_server_web.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:js_interop'; // this warning is pointless, since `web` and `js_interop` fail to compile on // native platforms, so they aren't safe to import without conditional // imports either (yet) // ignore: avoid_web_libraries_in_flutter -import 'dart:js_util' as js_util; import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; @@ -37,10 +37,10 @@ class KdfHttpServerOperations implements IKdfOperations { throw Exception('WebView controller is not available.'); } - final jsConfig = js_util.jsify({ + final jsConfig = { 'conf': startParams, 'log_level': logLevel ?? 3, - }); + }.jsify(); try { final result = await controller.evaluateJavascript(