Use JS library in Flutter

    Future<String> loadJS(String name) async {
  var givenJS = rootBundle.loadString('assets/$name.js');
  return givenJS.then((String js) {
    flutterWebViewPlugin.onStateChanged.listen((viewState) async {
      if (viewState.type == WebViewState.finishLoad) {
        flutterWebViewPlugin.evalJavascript(js);
      }
    });
  });
}

Honestly, if you're new to Flutter, Dart, and JS you are going to have some trouble with this unless you're willing to invest a fair amount of time. It does depend on what exactly you're trying to make with the Ether JS library, but in general you're going to have a hard time integrating it with flutter. There is an Ethereum package but it seems much narrower in scope than the ether.js library you've been looking at - it mostly seems focused on communication with the RPC api rather than dealing with wallets etc.

If you're dead set on using Flutter, your best bet would be to use Android & iOS specific libraries to do the actual ethereum stuff and to communicate through Platform Channels to a common api in your dart code. This could be a significant undertaking depending on how much of the API you need to expose, especially for someone new to flutter/dart and possibly to android/ios development as well. This will be much more performant than communicating back and forth with javascript running in a webview though, and realistically probably easier to code as well because flutter doesn't really have any good mechanisms for calling js code right now.

There is another option - to use a different client UI framework entirely. React native might do everything you need and has the advantage of being in Javascript so it can most likely integrate the Ether.js library easily, although I can't guarantee that it will actually fully support the ether.js library (its runtime might not have the necessary crypto extensions for example).


I eventually solved this by using Platform channels as suggested by rmtmckenzie in this answer.

I downloaded the JS file and saved it to android/app/src/main/res/raw/ether.js and ios/runner/ether.js for Android and iOS respectively.

Installing dependencies

Android

Add LiquidCore as a dependency in app level build.gradle

implementation 'com.github.LiquidPlayer:LiquidCore:0.5.0'

iOS

For iOS I used the JavaScriptCore which is part of the SDK.

Platform Channel

In my case, I needed to create a Wallet based on a Mnemonic (look up BIP39) I pass in into the JS function. For this, I created a Platform channel which passes in the Mnemonic (which is basically of type String) as an argument and will return a JSON object when done.

Future<dynamic> getWalletFromMnemonic({@required String mnemonic}) {
  return platform.invokeMethod('getWalletFromMnemonic', [mnemonic]);
}

Android Implementation (Java)

Inside MainActivity.java add this after this line

GeneratedPluginRegistrant.registerWith(this);

String CHANNEL = "UNIQUE_CHANNEL_NAME";

new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
    new MethodChannel.MethodCallHandler() {
        @Override
        public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
            if (methodCall.method.equals("getWalletFromMnemonic")) {
                ArrayList<Object> args = (ArrayList<Object>) methodCall.arguments;
                String mnemonic = (String) args.get(0);

                JSObject walletFromMnemonic = getWalletFromMnemonic(mnemonic);
                if (walletFromMnemonic == null) {
                    result.error("Could not create", "Wallet generation failed", null);
                    return;
                }

                String privateKey = walletFromMnemonic.property("privateKey").toString();
                String address = walletFromMnemonic.property("address").toString();

                HashMap<String, String> map = new HashMap<>();
                map.put("privateKey", privateKey);
                map.put("address", address);

                JSONObject obj = new JSONObject(map);

                result.success(obj.toString());

            } else {
                result.notImplemented();
            }
        }
    }
);

Declare the following methods which perform the actual action of interacting with the JS library and returning the result to the platform channel.

@Nullable
@VisibleForTesting
private JSObject getWalletFromMnemonic(String mnemonic) {
    JSContext jsContext = getJsContext(getEther());
    JSObject wallet = getWalletObject(jsContext);

    if (wallet == null) {
        return null;
    }

    if (!wallet.hasProperty("fromMnemonic")) {
        return null;
    }

    JSFunction walletFunction = wallet.property("fromMnemonic").toObject().toFunction();
    return walletFunction.call(null, mnemonic).toObject();
}

@Nullable
@VisibleForTesting
private JSObject getWalletObject(JSContext context) {
    JSObject jsEthers = context.property("ethers").toObject();
    if (jsEthers.hasProperty("Wallet")) {
        return jsEthers.property("Wallet").toObject();
    }
    return null;
}

@VisibleForTesting
String getEther() {
    String s = "";
    InputStream is = getResources().openRawResource(R.raw.ether);
    try {
        s = IOUtils.toString(is);
    } catch (IOException e) {
        s = null;
        e.printStackTrace();
    } finally {
        IOUtils.closeQuietly(is);
    }
    return s;
}

@VisibleForTesting
JSContext getJsContext(String code) {
    JSContext context = new JSContext();
    context.evaluateScript(code);
    return context;
}

iOS Implementation (Swift)

Add the following lines in AppDelegate.swift inside the override application method.

final let methodChannelName: String = "UNIQUE_CHANNEL_NAME"
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
let methodChannel = FlutterMethodChannel.init(name: methodChannelName, binaryMessenger: controller)

methodChannel.setMethodCallHandler({
    (call: FlutterMethodCall, result: FlutterResult)-> Void in
    if call.method == "getWalletFromMnemonic" {
        guard let mnemonic = call.arguments as? [String] else {
            return
        }

        if let wallet = self.getWalletFromMnemonic(mnemonic: mnemonic[0]) {
            result(wallet)
        } else {
            result("Invalid")
        }
    }
})

Add the logic to interact with the JavaScriptCore.

private func getWalletFromMnemonic(mnemonic: String) -> Dictionary<String, String>? {
    let PRIVATE_KEY = "privateKey"
    let ADDRESS = "address"

    guard let jsContext = self.initialiseJS(jsFileName: "ether") else { return nil }
    guard let etherObject = jsContext.objectForKeyedSubscript("ethers") else { return nil }
    guard let walletObject = etherObject.objectForKeyedSubscript("Wallet") else { return nil }


    guard let walletFromMnemonicObject = walletObject.objectForKeyedSubscript("fromMnemonic") else {
        return nil
    }

    guard let wallet = walletFromMnemonicObject.call(withArguments: [mnemonic]) else { return nil }
    guard let privateKey = wallet.forProperty(PRIVATE_KEY)?.toString() else { return nil }
    guard let address = wallet.forProperty(ADDRESS)?.toString() else { return nil }

    var walletDictionary = Dictionary<String, String>()
    walletDictionary[ADDRESS] = address
    walletDictionary[PRIVATE_KEY] = privateKey

    return walletDictionary
}

private func initialiseJS(jsFileName: String) -> JSContext? {
    let jsContext = JSContext()
    guard let jsSourcePath = Bundle.main.path(forResource: jsFileName, ofType: "js") else {
        return nil
    }
    do {
        let jsSourceContents = try String(contentsOfFile: jsSourcePath)
        jsContext!.evaluateScript(jsSourceContents)
        return jsContext!
    } catch {
        print(error.localizedDescription)
    }
    return nil
}

Tags:

Dart

Flutter