Use JS library in Flutter

后端 未结 3 1350
庸人自扰
庸人自扰 2020-12-23 12:29

I am trying to use Ether JS in my Flutter application. I know that it is not directly supported and even the existing implementations are not really well documented.

3条回答
  •  情深已故
    2020-12-23 13:02

    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 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 args = (ArrayList) 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 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? {
        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()
        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
    }
    

    提交回复
    热议问题