Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(kdf-wasm-ops): response type conversion and migrate to js_interop #14

Merged
merged 3 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading