Skip to content

Commit

Permalink
fix(kdf-wasm-ops): response type conversion and migrate to js_interop (
Browse files Browse the repository at this point in the history
…#14)

* migrate wasm ops to js_interop

improve type conversion and error handling

* remove runZoneGuarded from mm2Rpc function

* migrate remaining files to js_interop
  • Loading branch information
takenagain authored Dec 2, 2024
1 parent 23ff41d commit 2212b06
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 66 deletions.
2 changes: 1 addition & 1 deletion packages/komodo_defi_framework/app_build/build_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<KdfStartupResult> 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()) {
Expand Down Expand Up @@ -139,17 +126,16 @@ class KdfOperationsWasm implements IKdfOperations {
await _ensureLoaded();

try {
final errorOrNull =
await (js_util.dartify(_kdfModule!.callMethod('mm2_stop'.toJS))!
as Future<Object?>);
final errorOrNull = await (_kdfModule!
.callMethod('mm2_stop'.toJS)
.dartify()! as Future<Object?>);

if (errorOrNull is int) {
return StopStatus.fromDefaultInt(errorOrNull);
}

_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;

Expand All @@ -173,33 +159,112 @@ class KdfOperationsWasm implements IKdfOperations {

@override
Future<JsonMap> 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<js_interop.JSObject> _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<js_interop.JSObject>(
_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<String, dynamic>());
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<Object?, Object?>
Map<String, dynamic> _deepConvertMap(Map<dynamic, dynamic> 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<dynamic> _deepConvertList(List<dynamic> list) {
return list.map((value) {
if (value is Map) return _deepConvertMap(value);
if (value is List) return _deepConvertList(value);
return value;
}).toList();
}

@override
Future<void> validateSetup() async {
await _ensureLoaded();
Expand Down
56 changes: 38 additions & 18 deletions packages/komodo_defi_framework/web/res/kdf_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -57,38 +58,57 @@ class KdfPlugin {

final completer = Completer<void>();

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<int> _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<int> _mm2Stop() async {
await _ensureLoaded();
return js.context.callMethod('mm2_stop') as int;
final jsResult = globalContext.callMethod('mm2_stop'.toJS);
return jsResult.dartify()! as int;
}
}
6 changes: 3 additions & 3 deletions playground/lib/kdf_operations/kdf_operations_server_web.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 2212b06

Please sign in to comment.