From 7b68975a00ee914d91f9c6a56c55a357012ab844 Mon Sep 17 00:00:00 2001
From: Guillaume Ballet <gballet@gmail.com>
Date: Mon, 27 Jan 2020 11:50:48 +0100
Subject: [PATCH] console, internal/jsre: use github.com/dop251/goja (#20470)

This replaces the JavaScript interpreter used by the console with goja,
which is actively maintained and a lot faster than otto. Clef still uses otto
and eth/tracers still uses duktape, so we are currently dependent on three
different JS interpreters. We're looking to replace the remaining uses of otto
soon though.
---
 cmd/geth/consolecmd_test.go      |   8 +-
 console/bridge.go                | 373 +++++++++++++++++--------------
 console/console.go               | 207 +++++++++--------
 console/console_test.go          |   2 +-
 go.mod                           |   3 +
 go.sum                           |   8 +
 internal/jsre/completion.go      |  36 +--
 internal/jsre/completion_test.go |   4 +
 internal/jsre/jsre.go            | 187 +++++++---------
 internal/jsre/jsre_test.go       |  34 +--
 internal/jsre/pretty.go          | 174 ++++++++------
 11 files changed, 559 insertions(+), 477 deletions(-)

diff --git a/cmd/geth/consolecmd_test.go b/cmd/geth/consolecmd_test.go
index 45d4daff0..8f6a102d7 100644
--- a/cmd/geth/consolecmd_test.go
+++ b/cmd/geth/consolecmd_test.go
@@ -51,7 +51,9 @@ func TestConsoleWelcome(t *testing.T) {
 	geth.SetTemplateFunc("goarch", func() string { return runtime.GOARCH })
 	geth.SetTemplateFunc("gover", runtime.Version)
 	geth.SetTemplateFunc("gethver", func() string { return params.VersionWithCommit("", "") })
-	geth.SetTemplateFunc("niltime", func() string { return time.Unix(0, 0).Format(time.RFC1123) })
+	geth.SetTemplateFunc("niltime", func() string {
+		return time.Unix(0, 0).Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)")
+	})
 	geth.SetTemplateFunc("apis", func() string { return ipcAPIs })
 
 	// Verify the actual welcome message to the required template
@@ -142,7 +144,9 @@ func testAttachWelcome(t *testing.T, geth *testgeth, endpoint, apis string) {
 	attach.SetTemplateFunc("gover", runtime.Version)
 	attach.SetTemplateFunc("gethver", func() string { return params.VersionWithCommit("", "") })
 	attach.SetTemplateFunc("etherbase", func() string { return geth.Etherbase })
-	attach.SetTemplateFunc("niltime", func() string { return time.Unix(0, 0).Format(time.RFC1123) })
+	attach.SetTemplateFunc("niltime", func() string {
+		return time.Unix(0, 0).Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)")
+	})
 	attach.SetTemplateFunc("ipc", func() bool { return strings.HasPrefix(endpoint, "ipc") })
 	attach.SetTemplateFunc("datadir", func() string { return geth.Datadir })
 	attach.SetTemplateFunc("apis", func() string { return apis })
diff --git a/console/bridge.go b/console/bridge.go
index c7a67a685..2625c481d 100644
--- a/console/bridge.go
+++ b/console/bridge.go
@@ -20,14 +20,16 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"reflect"
 	"strings"
 	"time"
 
+	"github.com/dop251/goja"
 	"github.com/ethereum/go-ethereum/accounts/scwallet"
 	"github.com/ethereum/go-ethereum/accounts/usbwallet"
-	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/internal/jsre"
 	"github.com/ethereum/go-ethereum/rpc"
-	"github.com/robertkrimen/otto"
 )
 
 // bridge is a collection of JavaScript utility methods to bride the .js runtime
@@ -47,10 +49,18 @@ func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer) *br
 	}
 }
 
+func getJeth(vm *goja.Runtime) *goja.Object {
+	jeth := vm.Get("jeth")
+	if jeth == nil {
+		panic(vm.ToValue("jeth object does not exist"))
+	}
+	return jeth.ToObject(vm)
+}
+
 // NewAccount is a wrapper around the personal.newAccount RPC method that uses a
 // non-echoing password prompt to acquire the passphrase and executes the original
 // RPC method (saved in jeth.newAccount) with it to actually execute the RPC call.
-func (b *bridge) NewAccount(call otto.FunctionCall) (response otto.Value) {
+func (b *bridge) NewAccount(call jsre.Call) (goja.Value, error) {
 	var (
 		password string
 		confirm  string
@@ -58,52 +68,57 @@ func (b *bridge) NewAccount(call otto.FunctionCall) (response otto.Value) {
 	)
 	switch {
 	// No password was specified, prompt the user for it
-	case len(call.ArgumentList) == 0:
-		if password, err = b.prompter.PromptPassword("Password: "); err != nil {
-			throwJSException(err.Error())
+	case len(call.Arguments) == 0:
+		if password, err = b.prompter.PromptPassword("Passphrase: "); err != nil {
+			return nil, err
 		}
-		if confirm, err = b.prompter.PromptPassword("Repeat password: "); err != nil {
-			throwJSException(err.Error())
+		if confirm, err = b.prompter.PromptPassword("Repeat passphrase: "); err != nil {
+			return nil, err
 		}
 		if password != confirm {
-			throwJSException("passwords don't match!")
+			return nil, fmt.Errorf("passwords don't match!")
 		}
-
 	// A single string password was specified, use that
-	case len(call.ArgumentList) == 1 && call.Argument(0).IsString():
-		password, _ = call.Argument(0).ToString()
-
-	// Otherwise fail with some error
+	case len(call.Arguments) == 1 && call.Argument(0).ToString() != nil:
+		password = call.Argument(0).ToString().String()
 	default:
-		throwJSException("expected 0 or 1 string argument")
+		return nil, fmt.Errorf("expected 0 or 1 string argument")
 	}
 	// Password acquired, execute the call and return
-	ret, err := call.Otto.Call("jeth.newAccount", nil, password)
+	newAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("newAccount"))
+	if !callable {
+		return nil, fmt.Errorf("jeth.newAccount is not callable")
+	}
+	ret, err := newAccount(goja.Null(), call.VM.ToValue(password))
 	if err != nil {
-		throwJSException(err.Error())
+		return nil, err
 	}
-	return ret
+	return ret, nil
 }
 
 // OpenWallet is a wrapper around personal.openWallet which can interpret and
 // react to certain error messages, such as the Trezor PIN matrix request.
-func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) {
+func (b *bridge) OpenWallet(call jsre.Call) (goja.Value, error) {
 	// Make sure we have a wallet specified to open
-	if !call.Argument(0).IsString() {
-		throwJSException("first argument must be the wallet URL to open")
+	if call.Argument(0).ToObject(call.VM).ClassName() != "String" {
+		return nil, fmt.Errorf("first argument must be the wallet URL to open")
 	}
 	wallet := call.Argument(0)
 
-	var passwd otto.Value
-	if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() {
-		passwd, _ = otto.ToValue("")
+	var passwd goja.Value
+	if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
+		passwd = call.VM.ToValue("")
 	} else {
 		passwd = call.Argument(1)
 	}
 	// Open the wallet and return if successful in itself
-	val, err := call.Otto.Call("jeth.openWallet", nil, wallet, passwd)
+	openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
+	if !callable {
+		return nil, fmt.Errorf("jeth.openWallet is not callable")
+	}
+	val, err := openWallet(goja.Null(), wallet, passwd)
 	if err == nil {
-		return val
+		return val, nil
 	}
 
 	// Wallet open failed, report error unless it's a PIN or PUK entry
@@ -111,32 +126,31 @@ func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) {
 	case strings.HasSuffix(err.Error(), usbwallet.ErrTrezorPINNeeded.Error()):
 		val, err = b.readPinAndReopenWallet(call)
 		if err == nil {
-			return val
+			return val, nil
 		}
 		val, err = b.readPassphraseAndReopenWallet(call)
 		if err != nil {
-			throwJSException(err.Error())
+			return nil, err
 		}
 
 	case strings.HasSuffix(err.Error(), scwallet.ErrPairingPasswordNeeded.Error()):
 		// PUK input requested, fetch from the user and call open again
-		if input, err := b.prompter.PromptPassword("Please enter the pairing password: "); err != nil {
-			throwJSException(err.Error())
-		} else {
-			passwd, _ = otto.ToValue(input)
+		input, err := b.prompter.PromptPassword("Please enter the pairing password: ")
+		if err != nil {
+			return nil, err
 		}
-		if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil {
+		passwd = call.VM.ToValue(input)
+		if val, err = openWallet(goja.Null(), wallet, passwd); err != nil {
 			if !strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()) {
-				throwJSException(err.Error())
+				return nil, err
 			} else {
 				// PIN input requested, fetch from the user and call open again
-				if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil {
-					throwJSException(err.Error())
-				} else {
-					passwd, _ = otto.ToValue(input)
+				input, err := b.prompter.PromptPassword("Please enter current PIN: ")
+				if err != nil {
+					return nil, err
 				}
-				if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil {
-					throwJSException(err.Error())
+				if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(input)); err != nil {
+					return nil, err
 				}
 			}
 		}
@@ -144,52 +158,52 @@ func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) {
 	case strings.HasSuffix(err.Error(), scwallet.ErrPINUnblockNeeded.Error()):
 		// PIN unblock requested, fetch PUK and new PIN from the user
 		var pukpin string
-		if input, err := b.prompter.PromptPassword("Please enter current PUK: "); err != nil {
-			throwJSException(err.Error())
-		} else {
-			pukpin = input
+		input, err := b.prompter.PromptPassword("Please enter current PUK: ")
+		if err != nil {
+			return nil, err
 		}
-		if input, err := b.prompter.PromptPassword("Please enter new PIN: "); err != nil {
-			throwJSException(err.Error())
-		} else {
-			pukpin += input
+		pukpin = input
+		input, err = b.prompter.PromptPassword("Please enter new PIN: ")
+		if err != nil {
+			return nil, err
 		}
-		passwd, _ = otto.ToValue(pukpin)
-		if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil {
-			throwJSException(err.Error())
+		pukpin += input
+
+		if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(pukpin)); err != nil {
+			return nil, err
 		}
 
 	case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()):
 		// PIN input requested, fetch from the user and call open again
-		if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil {
-			throwJSException(err.Error())
-		} else {
-			passwd, _ = otto.ToValue(input)
+		input, err := b.prompter.PromptPassword("Please enter current PIN: ")
+		if err != nil {
+			return nil, err
 		}
-		if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil {
-			throwJSException(err.Error())
+		if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(input)); err != nil {
+			return nil, err
 		}
 
 	default:
 		// Unknown error occurred, drop to the user
-		throwJSException(err.Error())
+		return nil, err
 	}
-	return val
+	return val, nil
 }
 
-func (b *bridge) readPassphraseAndReopenWallet(call otto.FunctionCall) (otto.Value, error) {
-	var passwd otto.Value
+func (b *bridge) readPassphraseAndReopenWallet(call jsre.Call) (goja.Value, error) {
 	wallet := call.Argument(0)
-	if input, err := b.prompter.PromptPassword("Please enter your password: "); err != nil {
-		throwJSException(err.Error())
-	} else {
-		passwd, _ = otto.ToValue(input)
+	input, err := b.prompter.PromptPassword("Please enter your passphrase: ")
+	if err != nil {
+		return nil, err
+	}
+	openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
+	if !callable {
+		return nil, fmt.Errorf("jeth.openWallet is not callable")
 	}
-	return call.Otto.Call("jeth.openWallet", nil, wallet, passwd)
+	return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
 }
 
-func (b *bridge) readPinAndReopenWallet(call otto.FunctionCall) (otto.Value, error) {
-	var passwd otto.Value
+func (b *bridge) readPinAndReopenWallet(call jsre.Call) (goja.Value, error) {
 	wallet := call.Argument(0)
 	// Trezor PIN matrix input requested, display the matrix to the user and fetch the data
 	fmt.Fprintf(b.printer, "Look at the device for number positions\n\n")
@@ -199,155 +213,154 @@ func (b *bridge) readPinAndReopenWallet(call otto.FunctionCall) (otto.Value, err
 	fmt.Fprintf(b.printer, "--+---+--\n")
 	fmt.Fprintf(b.printer, "1 | 2 | 3\n\n")
 
-	if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil {
-		throwJSException(err.Error())
-	} else {
-		passwd, _ = otto.ToValue(input)
+	input, err := b.prompter.PromptPassword("Please enter current PIN: ")
+	if err != nil {
+		return nil, err
+	}
+	openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
+	if !callable {
+		return nil, fmt.Errorf("jeth.openWallet is not callable")
 	}
-	return call.Otto.Call("jeth.openWallet", nil, wallet, passwd)
+	return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
 }
 
 // UnlockAccount is a wrapper around the personal.unlockAccount RPC method that
 // uses a non-echoing password prompt to acquire the passphrase and executes the
 // original RPC method (saved in jeth.unlockAccount) with it to actually execute
 // the RPC call.
-func (b *bridge) UnlockAccount(call otto.FunctionCall) (response otto.Value) {
-	// Make sure we have an account specified to unlock
-	if !call.Argument(0).IsString() {
-		throwJSException("first argument must be the account to unlock")
+func (b *bridge) UnlockAccount(call jsre.Call) (goja.Value, error) {
+	// Make sure we have an account specified to unlock.
+	if call.Argument(0).ExportType().Kind() != reflect.String {
+		return nil, fmt.Errorf("first argument must be the account to unlock")
 	}
 	account := call.Argument(0)
 
-	// If password is not given or is the null value, prompt the user for it
-	var passwd otto.Value
-
-	if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() {
+	// If password is not given or is the null value, prompt the user for it.
+	var passwd goja.Value
+	if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
 		fmt.Fprintf(b.printer, "Unlock account %s\n", account)
-		if input, err := b.prompter.PromptPassword("Password: "); err != nil {
-			throwJSException(err.Error())
-		} else {
-			passwd, _ = otto.ToValue(input)
+		input, err := b.prompter.PromptPassword("Passphrase: ")
+		if err != nil {
+			return nil, err
 		}
+		passwd = call.VM.ToValue(input)
 	} else {
-		if !call.Argument(1).IsString() {
-			throwJSException("password must be a string")
+		if call.Argument(1).ExportType().Kind() != reflect.String {
+			return nil, fmt.Errorf("password must be a string")
 		}
 		passwd = call.Argument(1)
 	}
-	// Third argument is the duration how long the account must be unlocked.
-	duration := otto.NullValue()
-	if call.Argument(2).IsDefined() && !call.Argument(2).IsNull() {
-		if !call.Argument(2).IsNumber() {
-			throwJSException("unlock duration must be a number")
+
+	// Third argument is the duration how long the account should be unlocked.
+	duration := goja.Null()
+	if !goja.IsUndefined(call.Argument(2)) && !goja.IsNull(call.Argument(2)) {
+		if !isNumber(call.Argument(2)) {
+			return nil, fmt.Errorf("unlock duration must be a number")
 		}
 		duration = call.Argument(2)
 	}
-	// Send the request to the backend and return
-	val, err := call.Otto.Call("jeth.unlockAccount", nil, account, passwd, duration)
-	if err != nil {
-		throwJSException(err.Error())
+
+	// Send the request to the backend and return.
+	unlockAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockAccount"))
+	if !callable {
+		return nil, fmt.Errorf("jeth.unlockAccount is not callable")
 	}
-	return val
+	return unlockAccount(goja.Null(), account, passwd, duration)
 }
 
 // Sign is a wrapper around the personal.sign RPC method that uses a non-echoing password
 // prompt to acquire the passphrase and executes the original RPC method (saved in
 // jeth.sign) with it to actually execute the RPC call.
-func (b *bridge) Sign(call otto.FunctionCall) (response otto.Value) {
+func (b *bridge) Sign(call jsre.Call) (goja.Value, error) {
 	var (
 		message = call.Argument(0)
 		account = call.Argument(1)
 		passwd  = call.Argument(2)
 	)
 
-	if !message.IsString() {
-		throwJSException("first argument must be the message to sign")
+	if message.ExportType().Kind() != reflect.String {
+		return nil, fmt.Errorf("first argument must be the message to sign")
 	}
-	if !account.IsString() {
-		throwJSException("second argument must be the account to sign with")
+	if account.ExportType().Kind() != reflect.String {
+		return nil, fmt.Errorf("second argument must be the account to sign with")
 	}
 
 	// if the password is not given or null ask the user and ensure password is a string
-	if passwd.IsUndefined() || passwd.IsNull() {
+	if goja.IsUndefined(passwd) || goja.IsNull(passwd) {
 		fmt.Fprintf(b.printer, "Give password for account %s\n", account)
-		if input, err := b.prompter.PromptPassword("Password: "); err != nil {
-			throwJSException(err.Error())
-		} else {
-			passwd, _ = otto.ToValue(input)
+		input, err := b.prompter.PromptPassword("Password: ")
+		if err != nil {
+			return nil, err
 		}
-	}
-	if !passwd.IsString() {
-		throwJSException("third argument must be the password to unlock the account")
+		passwd = call.VM.ToValue(input)
+	} else if passwd.ExportType().Kind() != reflect.String {
+		return nil, fmt.Errorf("third argument must be the password to unlock the account")
 	}
 
 	// Send the request to the backend and return
-	val, err := call.Otto.Call("jeth.sign", nil, message, account, passwd)
-	if err != nil {
-		throwJSException(err.Error())
+	sign, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockAccount"))
+	if !callable {
+		return nil, fmt.Errorf("jeth.unlockAccount is not callable")
 	}
-	return val
+	return sign(goja.Null(), message, account, passwd)
 }
 
 // Sleep will block the console for the specified number of seconds.
-func (b *bridge) Sleep(call otto.FunctionCall) (response otto.Value) {
-	if call.Argument(0).IsNumber() {
-		sleep, _ := call.Argument(0).ToInteger()
-		time.Sleep(time.Duration(sleep) * time.Second)
-		return otto.TrueValue()
+func (b *bridge) Sleep(call jsre.Call) (goja.Value, error) {
+	if !isNumber(call.Argument(0)) {
+		return nil, fmt.Errorf("usage: sleep(<number of seconds>)")
 	}
-	return throwJSException("usage: sleep(<number of seconds>)")
+	sleep := call.Argument(0).ToFloat()
+	time.Sleep(time.Duration(sleep * float64(time.Second)))
+	return call.VM.ToValue(true), nil
 }
 
 // SleepBlocks will block the console for a specified number of new blocks optionally
 // until the given timeout is reached.
-func (b *bridge) SleepBlocks(call otto.FunctionCall) (response otto.Value) {
+func (b *bridge) SleepBlocks(call jsre.Call) (goja.Value, error) {
+	// Parse the input parameters for the sleep.
 	var (
 		blocks = int64(0)
 		sleep  = int64(9999999999999999) // indefinitely
 	)
-	// Parse the input parameters for the sleep
-	nArgs := len(call.ArgumentList)
+	nArgs := len(call.Arguments)
 	if nArgs == 0 {
-		throwJSException("usage: sleepBlocks(<n blocks>[, max sleep in seconds])")
+		return nil, fmt.Errorf("usage: sleepBlocks(<n blocks>[, max sleep in seconds])")
 	}
 	if nArgs >= 1 {
-		if call.Argument(0).IsNumber() {
-			blocks, _ = call.Argument(0).ToInteger()
-		} else {
-			throwJSException("expected number as first argument")
+		if !isNumber(call.Argument(0)) {
+			return nil, fmt.Errorf("expected number as first argument")
 		}
+		blocks = call.Argument(0).ToInteger()
 	}
 	if nArgs >= 2 {
-		if call.Argument(1).IsNumber() {
-			sleep, _ = call.Argument(1).ToInteger()
-		} else {
-			throwJSException("expected number as second argument")
+		if isNumber(call.Argument(1)) {
+			return nil, fmt.Errorf("expected number as second argument")
 		}
+		sleep = call.Argument(1).ToInteger()
 	}
-	// go through the console, this will allow web3 to call the appropriate
-	// callbacks if a delayed response or notification is received.
-	blockNumber := func() int64 {
-		result, err := call.Otto.Run("eth.blockNumber")
+
+	// Poll the current block number until either it or a timeout is reached.
+	var (
+		deadline   = time.Now().Add(time.Duration(sleep) * time.Second)
+		lastNumber = ^hexutil.Uint64(0)
+	)
+	for time.Now().Before(deadline) {
+		var number hexutil.Uint64
+		err := b.client.Call(&number, "eth_blockNumber")
 		if err != nil {
-			throwJSException(err.Error())
+			return nil, err
 		}
-		block, err := result.ToInteger()
-		if err != nil {
-			throwJSException(err.Error())
+		if number != lastNumber {
+			lastNumber = number
+			blocks--
 		}
-		return block
-	}
-	// Poll the current block number until either it ot a timeout is reached
-	targetBlockNr := blockNumber() + blocks
-	deadline := time.Now().Add(time.Duration(sleep) * time.Second)
-
-	for time.Now().Before(deadline) {
-		if blockNumber() >= targetBlockNr {
-			return otto.TrueValue()
+		if blocks <= 0 {
+			break
 		}
 		time.Sleep(time.Second)
 	}
-	return otto.FalseValue()
+	return call.VM.ToValue(true), nil
 }
 
 type jsonrpcCall struct {
@@ -357,15 +370,15 @@ type jsonrpcCall struct {
 }
 
 // Send implements the web3 provider "send" method.
-func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) {
+func (b *bridge) Send(call jsre.Call) (goja.Value, error) {
 	// Remarshal the request into a Go value.
-	JSON, _ := call.Otto.Object("JSON")
-	reqVal, err := JSON.Call("stringify", call.Argument(0))
+	reqVal, err := call.Argument(0).ToObject(call.VM).MarshalJSON()
 	if err != nil {
-		throwJSException(err.Error())
+		return nil, err
 	}
+
 	var (
-		rawReq = reqVal.String()
+		rawReq = string(reqVal)
 		dec    = json.NewDecoder(strings.NewReader(rawReq))
 		reqs   []jsonrpcCall
 		batch  bool
@@ -381,10 +394,12 @@ func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) {
 	}
 
 	// Execute the requests.
-	resps, _ := call.Otto.Object("new Array()")
+	var resps []*goja.Object
 	for _, req := range reqs {
-		resp, _ := call.Otto.Object(`({"jsonrpc":"2.0"})`)
+		resp := call.VM.NewObject()
+		resp.Set("jsonrpc", "2.0")
 		resp.Set("id", req.ID)
+
 		var result json.RawMessage
 		err = b.client.Call(&result, req.Method, req.Params...)
 		switch err := err.(type) {
@@ -392,9 +407,14 @@ func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) {
 			if result == nil {
 				// Special case null because it is decoded as an empty
 				// raw message for some reason.
-				resp.Set("result", otto.NullValue())
+				resp.Set("result", goja.Null())
 			} else {
-				resultVal, err := JSON.Call("parse", string(result))
+				JSON := call.VM.Get("JSON").ToObject(call.VM)
+				parse, callable := goja.AssertFunction(JSON.Get("parse"))
+				if !callable {
+					return nil, fmt.Errorf("JSON.parse is not a function")
+				}
+				resultVal, err := parse(goja.Null(), call.VM.ToValue(string(result)))
 				if err != nil {
 					setError(resp, -32603, err.Error())
 				} else {
@@ -406,33 +426,38 @@ func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) {
 		default:
 			setError(resp, -32603, err.Error())
 		}
-		resps.Call("push", resp)
+		resps = append(resps, resp)
 	}
 
 	// Return the responses either to the callback (if supplied)
 	// or directly as the return value.
+	var result goja.Value
 	if batch {
-		response = resps.Value()
+		result = call.VM.ToValue(resps)
 	} else {
-		response, _ = resps.Get("0")
+		result = resps[0]
 	}
-	if fn := call.Argument(1); fn.Class() == "Function" {
-		fn.Call(otto.NullValue(), otto.NullValue(), response)
-		return otto.UndefinedValue()
+	if fn, isFunc := goja.AssertFunction(call.Argument(1)); isFunc {
+		fn(goja.Null(), goja.Null(), result)
+		return goja.Undefined(), nil
 	}
-	return response
+	return result, nil
 }
 
-func setError(resp *otto.Object, code int, msg string) {
+func setError(resp *goja.Object, code int, msg string) {
 	resp.Set("error", map[string]interface{}{"code": code, "message": msg})
 }
 
-// throwJSException panics on an otto.Value. The Otto VM will recover from the
-// Go panic and throw msg as a JavaScript error.
-func throwJSException(msg interface{}) otto.Value {
-	val, err := otto.ToValue(msg)
-	if err != nil {
-		log.Error("Failed to serialize JavaScript exception", "exception", msg, "err", err)
+// isNumber returns true if input value is a JS number.
+func isNumber(v goja.Value) bool {
+	k := v.ExportType().Kind()
+	return k >= reflect.Int && k <= reflect.Float64
+}
+
+func getObject(vm *goja.Runtime, name string) *goja.Object {
+	v := vm.Get(name)
+	if v == nil {
+		return nil
 	}
-	panic(val)
+	return v.ToObject(vm)
 }
diff --git a/console/console.go b/console/console.go
index 5326ed2c8..a2f12d8ed 100644
--- a/console/console.go
+++ b/console/console.go
@@ -28,12 +28,13 @@ import (
 	"strings"
 	"syscall"
 
+	"github.com/dop251/goja"
 	"github.com/ethereum/go-ethereum/internal/jsre"
+	"github.com/ethereum/go-ethereum/internal/jsre/deps"
 	"github.com/ethereum/go-ethereum/internal/web3ext"
 	"github.com/ethereum/go-ethereum/rpc"
 	"github.com/mattn/go-colorable"
 	"github.com/peterh/liner"
-	"github.com/robertkrimen/otto"
 )
 
 var (
@@ -86,6 +87,7 @@ func New(config Config) (*Console, error) {
 	if config.Printer == nil {
 		config.Printer = colorable.NewColorableStdout()
 	}
+
 	// Initialize the console and return
 	console := &Console{
 		client:   config.Client,
@@ -107,120 +109,141 @@ func New(config Config) (*Console, error) {
 // init retrieves the available APIs from the remote RPC provider and initializes
 // the console's JavaScript namespaces based on the exposed modules.
 func (c *Console) init(preload []string) error {
-	// Initialize the JavaScript <-> Go RPC bridge
+	c.initConsoleObject()
+
+	// Initialize the JavaScript <-> Go RPC bridge.
 	bridge := newBridge(c.client, c.prompter, c.printer)
-	c.jsre.Set("jeth", struct{}{})
+	if err := c.initWeb3(bridge); err != nil {
+		return err
+	}
+	if err := c.initExtensions(); err != nil {
+		return err
+	}
+
+	// Add bridge overrides for web3.js functionality.
+	c.jsre.Do(func(vm *goja.Runtime) {
+		c.initAdmin(vm, bridge)
+		c.initPersonal(vm, bridge)
+	})
+
+	// Preload JavaScript files.
+	for _, path := range preload {
+		if err := c.jsre.Exec(path); err != nil {
+			failure := err.Error()
+			if gojaErr, ok := err.(*goja.Exception); ok {
+				failure = gojaErr.String()
+			}
+			return fmt.Errorf("%s: %v", path, failure)
+		}
+	}
 
-	jethObj, _ := c.jsre.Get("jeth")
-	jethObj.Object().Set("send", bridge.Send)
-	jethObj.Object().Set("sendAsync", bridge.Send)
+	// Configure the input prompter for history and tab completion.
+	if c.prompter != nil {
+		if content, err := ioutil.ReadFile(c.histPath); err != nil {
+			c.prompter.SetHistory(nil)
+		} else {
+			c.history = strings.Split(string(content), "\n")
+			c.prompter.SetHistory(c.history)
+		}
+		c.prompter.SetWordCompleter(c.AutoCompleteInput)
+	}
+	return nil
+}
 
-	consoleObj, _ := c.jsre.Get("console")
-	consoleObj.Object().Set("log", c.consoleOutput)
-	consoleObj.Object().Set("error", c.consoleOutput)
+func (c *Console) initConsoleObject() {
+	c.jsre.Do(func(vm *goja.Runtime) {
+		console := vm.NewObject()
+		console.Set("log", c.consoleOutput)
+		console.Set("error", c.consoleOutput)
+		vm.Set("console", console)
+	})
+}
 
-	// Load all the internal utility JavaScript libraries
-	if err := c.jsre.Compile("bignumber.js", jsre.BignumberJs); err != nil {
+func (c *Console) initWeb3(bridge *bridge) error {
+	bnJS := string(deps.MustAsset("bignumber.js"))
+	web3JS := string(deps.MustAsset("web3.js"))
+	if err := c.jsre.Compile("bignumber.js", bnJS); err != nil {
 		return fmt.Errorf("bignumber.js: %v", err)
 	}
-	if err := c.jsre.Compile("web3.js", jsre.Web3Js); err != nil {
+	if err := c.jsre.Compile("web3.js", web3JS); err != nil {
 		return fmt.Errorf("web3.js: %v", err)
 	}
 	if _, err := c.jsre.Run("var Web3 = require('web3');"); err != nil {
 		return fmt.Errorf("web3 require: %v", err)
 	}
-	if _, err := c.jsre.Run("var web3 = new Web3(jeth);"); err != nil {
-		return fmt.Errorf("web3 provider: %v", err)
-	}
-	// Load the supported APIs into the JavaScript runtime environment
+	var err error
+	c.jsre.Do(func(vm *goja.Runtime) {
+		transport := vm.NewObject()
+		transport.Set("send", jsre.MakeCallback(vm, bridge.Send))
+		transport.Set("sendAsync", jsre.MakeCallback(vm, bridge.Send))
+		vm.Set("_consoleWeb3Transport", transport)
+		_, err = vm.RunString("var web3 = new Web3(_consoleWeb3Transport)")
+	})
+	return err
+}
+
+// initExtensions loads and registers web3.js extensions.
+func (c *Console) initExtensions() error {
+	// Compute aliases from server-provided modules.
 	apis, err := c.client.SupportedModules()
 	if err != nil {
 		return fmt.Errorf("api modules: %v", err)
 	}
-	flatten := "var eth = web3.eth; var personal = web3.personal; "
+	aliases := map[string]struct{}{"eth": {}, "personal": {}}
 	for api := range apis {
 		if api == "web3" {
-			continue // manually mapped or ignore
+			continue
 		}
+		aliases[api] = struct{}{}
 		if file, ok := web3ext.Modules[api]; ok {
-			// Load our extension for the module.
-			if err = c.jsre.Compile(fmt.Sprintf("%s.js", api), file); err != nil {
+			if err = c.jsre.Compile(api+".js", file); err != nil {
 				return fmt.Errorf("%s.js: %v", api, err)
 			}
-			flatten += fmt.Sprintf("var %s = web3.%s; ", api, api)
-		} else if obj, err := c.jsre.Run("web3." + api); err == nil && obj.IsObject() {
-			// Enable web3.js built-in extension if available.
-			flatten += fmt.Sprintf("var %s = web3.%s; ", api, api)
 		}
 	}
-	if _, err = c.jsre.Run(flatten); err != nil {
-		return fmt.Errorf("namespace flattening: %v", err)
-	}
-	// Initialize the global name register (disabled for now)
-	//c.jsre.Run(`var GlobalRegistrar = eth.contract(` + registrar.GlobalRegistrarAbi + `);   registrar = GlobalRegistrar.at("` + registrar.GlobalRegistrarAddr + `");`)
 
-	// If the console is in interactive mode, instrument password related methods to query the user
-	if c.prompter != nil {
-		// Retrieve the account management object to instrument
-		personal, err := c.jsre.Get("personal")
-		if err != nil {
-			return err
-		}
-		// Override the openWallet, unlockAccount, newAccount and sign methods since
-		// these require user interaction. Assign these method in the Console the
-		// original web3 callbacks. These will be called by the jeth.* methods after
-		// they got the password from the user and send the original web3 request to
-		// the backend.
-		if obj := personal.Object(); obj != nil { // make sure the personal api is enabled over the interface
-			if _, err = c.jsre.Run(`jeth.openWallet = personal.openWallet;`); err != nil {
-				return fmt.Errorf("personal.openWallet: %v", err)
-			}
-			if _, err = c.jsre.Run(`jeth.unlockAccount = personal.unlockAccount;`); err != nil {
-				return fmt.Errorf("personal.unlockAccount: %v", err)
-			}
-			if _, err = c.jsre.Run(`jeth.newAccount = personal.newAccount;`); err != nil {
-				return fmt.Errorf("personal.newAccount: %v", err)
-			}
-			if _, err = c.jsre.Run(`jeth.sign = personal.sign;`); err != nil {
-				return fmt.Errorf("personal.sign: %v", err)
-			}
-			obj.Set("openWallet", bridge.OpenWallet)
-			obj.Set("unlockAccount", bridge.UnlockAccount)
-			obj.Set("newAccount", bridge.NewAccount)
-			obj.Set("sign", bridge.Sign)
-		}
-	}
-	// The admin.sleep and admin.sleepBlocks are offered by the console and not by the RPC layer.
-	admin, err := c.jsre.Get("admin")
-	if err != nil {
-		return err
-	}
-	if obj := admin.Object(); obj != nil { // make sure the admin api is enabled over the interface
-		obj.Set("sleepBlocks", bridge.SleepBlocks)
-		obj.Set("sleep", bridge.Sleep)
-		obj.Set("clearHistory", c.clearHistory)
-	}
-	// Preload any JavaScript files before starting the console
-	for _, path := range preload {
-		if err := c.jsre.Exec(path); err != nil {
-			failure := err.Error()
-			if ottoErr, ok := err.(*otto.Error); ok {
-				failure = ottoErr.String()
+	// Apply aliases.
+	c.jsre.Do(func(vm *goja.Runtime) {
+		web3 := getObject(vm, "web3")
+		for name := range aliases {
+			if v := web3.Get(name); v != nil {
+				vm.Set(name, v)
 			}
-			return fmt.Errorf("%s: %v", path, failure)
 		}
+	})
+	return nil
+}
+
+// initAdmin creates additional admin APIs implemented by the bridge.
+func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
+	if admin := getObject(vm, "admin"); admin != nil {
+		admin.Set("sleepBlocks", jsre.MakeCallback(vm, bridge.SleepBlocks))
+		admin.Set("sleep", jsre.MakeCallback(vm, bridge.Sleep))
+		admin.Set("clearHistory", c.clearHistory)
 	}
-	// Configure the console's input prompter for scrollback and tab completion
-	if c.prompter != nil {
-		if content, err := ioutil.ReadFile(c.histPath); err != nil {
-			c.prompter.SetHistory(nil)
-		} else {
-			c.history = strings.Split(string(content), "\n")
-			c.prompter.SetHistory(c.history)
-		}
-		c.prompter.SetWordCompleter(c.AutoCompleteInput)
+}
+
+// initPersonal redirects account-related API methods through the bridge.
+//
+// If the console is in interactive mode and the 'personal' API is available, override
+// the openWallet, unlockAccount, newAccount and sign methods since these require user
+// interaction. The original web3 callbacks are stored in 'jeth'. These will be called
+// by the bridge after the prompt and send the original web3 request to the backend.
+func (c *Console) initPersonal(vm *goja.Runtime, bridge *bridge) {
+	personal := getObject(vm, "personal")
+	if personal == nil || c.prompter == nil {
+		return
 	}
-	return nil
+	jeth := vm.NewObject()
+	vm.Set("jeth", jeth)
+	jeth.Set("openWallet", personal.Get("openWallet"))
+	jeth.Set("unlockAccount", personal.Get("unlockAccount"))
+	jeth.Set("newAccount", personal.Get("newAccount"))
+	jeth.Set("sign", personal.Get("sign"))
+	personal.Set("openWallet", jsre.MakeCallback(vm, bridge.OpenWallet))
+	personal.Set("unlockAccount", jsre.MakeCallback(vm, bridge.UnlockAccount))
+	personal.Set("newAccount", jsre.MakeCallback(vm, bridge.NewAccount))
+	personal.Set("sign", jsre.MakeCallback(vm, bridge.Sign))
 }
 
 func (c *Console) clearHistory() {
@@ -235,13 +258,13 @@ func (c *Console) clearHistory() {
 
 // consoleOutput is an override for the console.log and console.error methods to
 // stream the output into the configured output stream instead of stdout.
-func (c *Console) consoleOutput(call otto.FunctionCall) otto.Value {
+func (c *Console) consoleOutput(call goja.FunctionCall) goja.Value {
 	var output []string
-	for _, argument := range call.ArgumentList {
+	for _, argument := range call.Arguments {
 		output = append(output, fmt.Sprintf("%v", argument))
 	}
 	fmt.Fprintln(c.printer, strings.Join(output, " "))
-	return otto.Value{}
+	return goja.Null()
 }
 
 // AutoCompleteInput is a pre-assembled word completer to be used by the user
@@ -304,13 +327,13 @@ func (c *Console) Welcome() {
 
 // Evaluate executes code and pretty prints the result to the specified output
 // stream.
-func (c *Console) Evaluate(statement string) error {
+func (c *Console) Evaluate(statement string) {
 	defer func() {
 		if r := recover(); r != nil {
 			fmt.Fprintf(c.printer, "[native] error: %v\n", r)
 		}
 	}()
-	return c.jsre.Evaluate(statement, c.printer)
+	c.jsre.Evaluate(statement, c.printer)
 }
 
 // Interactive starts an interactive user session, where input is propted from
diff --git a/console/console_test.go b/console/console_test.go
index 89dd7cd83..9a2b47444 100644
--- a/console/console_test.go
+++ b/console/console_test.go
@@ -289,7 +289,7 @@ func TestPrettyError(t *testing.T) {
 	defer tester.Close(t)
 	tester.console.Evaluate("throw 'hello'")
 
-	want := jsre.ErrorColor("hello") + "\n"
+	want := jsre.ErrorColor("hello") + "\n\tat <eval>:1:7(1)\n\n"
 	if output := tester.output.String(); output != want {
 		t.Fatalf("pretty error mismatch: have %s, want %s", output, want)
 	}
diff --git a/go.mod b/go.mod
index 223086f8c..c3b5b6fd0 100644
--- a/go.mod
+++ b/go.mod
@@ -16,13 +16,16 @@ require (
 	github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9
 	github.com/davecgh/go-spew v1.1.1
 	github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea
+	github.com/dlclark/regexp2 v1.2.0 // indirect
 	github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf
+	github.com/dop251/goja v0.0.0-20200106141417-aaec0e7bde29
 	github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c
 	github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa
 	github.com/fatih/color v1.3.0
 	github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc
 	github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff
 	github.com/go-ole/go-ole v1.2.1 // indirect
+	github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect
 	github.com/go-stack/stack v1.8.0
 	github.com/golang/protobuf v1.3.2-0.20190517061210-b285ee9cfc6c
 	github.com/golang/snappy v0.0.1
diff --git a/go.sum b/go.sum
index 4d18c8c20..81c718b02 100644
--- a/go.sum
+++ b/go.sum
@@ -57,8 +57,14 @@ github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vs
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
+github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf h1:sh8rkQZavChcmakYiSlqu2425CHyFXLZZnvm7PDpU8M=
 github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/dop251/goja v0.0.0-20191203121440-007eef3bc40f h1:vtCDQseO/Sbu5IZSoc2uzZ7CkSoai7OtpcwGFK5FlyE=
+github.com/dop251/goja v0.0.0-20191203121440-007eef3bc40f/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
+github.com/dop251/goja v0.0.0-20200106141417-aaec0e7bde29 h1:Ewd9K+mC725sITA12QQHRqWj78NU4t7EhlFVVgdlzJg=
+github.com/dop251/goja v0.0.0-20200106141417-aaec0e7bde29/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
 github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c h1:JHHhtb9XWJrGNMcrVP6vyzO4dusgi/HnceHTgxSejUM=
 github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
 github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa h1:XKAhUk/dtp+CV0VO6mhG2V7jA9vbcGcnYF/Ay9NjZrY=
@@ -77,6 +83,8 @@ github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2i
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
 github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
+github.com/go-sourcemap/sourcemap v2.1.2+incompatible h1:0b/xya7BKGhXuqFESKM4oIiRo9WOt2ebz7KxfreD6ug=
+github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
 github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
diff --git a/internal/jsre/completion.go b/internal/jsre/completion.go
index 7f484bbbb..2c105184c 100644
--- a/internal/jsre/completion.go
+++ b/internal/jsre/completion.go
@@ -20,35 +20,43 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/robertkrimen/otto"
+	"github.com/dop251/goja"
 )
 
 // CompleteKeywords returns potential continuations for the given line. Since line is
 // evaluated, callers need to make sure that evaluating line does not have side effects.
 func (jsre *JSRE) CompleteKeywords(line string) []string {
 	var results []string
-	jsre.Do(func(vm *otto.Otto) {
+	jsre.Do(func(vm *goja.Runtime) {
 		results = getCompletions(vm, line)
 	})
 	return results
 }
 
-func getCompletions(vm *otto.Otto, line string) (results []string) {
+func getCompletions(vm *goja.Runtime, line string) (results []string) {
 	parts := strings.Split(line, ".")
-	objRef := "this"
-	prefix := line
-	if len(parts) > 1 {
-		objRef = strings.Join(parts[0:len(parts)-1], ".")
-		prefix = parts[len(parts)-1]
+	if len(parts) == 0 {
+		return nil
 	}
 
-	obj, _ := vm.Object(objRef)
-	if obj == nil {
-		return nil
+	// Find the right-most fully named object in the line. e.g. if line = "x.y.z"
+	// and "x.y" is an object, obj will reference "x.y".
+	obj := vm.GlobalObject()
+	for i := 0; i < len(parts)-1; i++ {
+		v := obj.Get(parts[i])
+		if v == nil {
+			return nil // No object was found
+		}
+		obj = v.ToObject(vm)
 	}
+
+	// Go over the keys of the object and retain the keys matching prefix.
+	// Example: if line = "x.y.z" and "x.y" exists and has keys "zebu", "zebra"
+	// and "platypus", then "x.y.zebu" and "x.y.zebra" will be added to results.
+	prefix := parts[len(parts)-1]
 	iterOwnAndConstructorKeys(vm, obj, func(k string) {
 		if strings.HasPrefix(k, prefix) {
-			if objRef == "this" {
+			if len(parts) == 1 {
 				results = append(results, k)
 			} else {
 				results = append(results, strings.Join(parts[:len(parts)-1], ".")+"."+k)
@@ -59,9 +67,9 @@ func getCompletions(vm *otto.Otto, line string) (results []string) {
 	// Append opening parenthesis (for functions) or dot (for objects)
 	// if the line itself is the only completion.
 	if len(results) == 1 && results[0] == line {
-		obj, _ := vm.Object(line)
+		obj := obj.Get(parts[len(parts)-1])
 		if obj != nil {
-			if obj.Class() == "Function" {
+			if _, isfunc := goja.AssertFunction(obj); isfunc {
 				results[0] += "("
 			} else {
 				results[0] += "."
diff --git a/internal/jsre/completion_test.go b/internal/jsre/completion_test.go
index ccbd73dcc..2d05547d1 100644
--- a/internal/jsre/completion_test.go
+++ b/internal/jsre/completion_test.go
@@ -39,6 +39,10 @@ func TestCompleteKeywords(t *testing.T) {
 		input string
 		want  []string
 	}{
+		{
+			input: "St",
+			want:  []string{"String"},
+		},
 		{
 			input: "x",
 			want:  []string{"x."},
diff --git a/internal/jsre/jsre.go b/internal/jsre/jsre.go
index 1b3528a03..bc8869b25 100644
--- a/internal/jsre/jsre.go
+++ b/internal/jsre/jsre.go
@@ -26,30 +26,30 @@ import (
 	"math/rand"
 	"time"
 
+	"github.com/dop251/goja"
 	"github.com/ethereum/go-ethereum/common"
-	"github.com/ethereum/go-ethereum/internal/jsre/deps"
-	"github.com/robertkrimen/otto"
 )
 
-var (
-	BignumberJs = deps.MustAsset("bignumber.js")
-	Web3Js      = deps.MustAsset("web3.js")
-)
-
-/*
-JSRE is a generic JS runtime environment embedding the otto JS interpreter.
-It provides some helper functions to
-- load code from files
-- run code snippets
-- require libraries
-- bind native go objects
-*/
+// JSRE is a JS runtime environment embedding the goja interpreter.
+// It provides helper functions to load code from files, run code snippets
+// and bind native go objects to JS.
+//
+// The runtime runs all code on a dedicated event loop and does not expose the underlying
+// goja runtime directly. To use the runtime, call JSRE.Do. When binding a Go function,
+// use the Call type to gain access to the runtime.
 type JSRE struct {
 	assetPath     string
 	output        io.Writer
 	evalQueue     chan *evalReq
 	stopEventLoop chan bool
 	closed        chan struct{}
+	vm            *goja.Runtime
+}
+
+// Call is the argument type of Go functions which are callable from JS.
+type Call struct {
+	goja.FunctionCall
+	VM *goja.Runtime
 }
 
 // jsTimer is a single timer instance with a callback function
@@ -57,12 +57,12 @@ type jsTimer struct {
 	timer    *time.Timer
 	duration time.Duration
 	interval bool
-	call     otto.FunctionCall
+	call     goja.FunctionCall
 }
 
 // evalReq is a serialized vm execution request processed by runEventLoop.
 type evalReq struct {
-	fn   func(vm *otto.Otto)
+	fn   func(vm *goja.Runtime)
 	done chan bool
 }
 
@@ -74,9 +74,10 @@ func New(assetPath string, output io.Writer) *JSRE {
 		closed:        make(chan struct{}),
 		evalQueue:     make(chan *evalReq),
 		stopEventLoop: make(chan bool),
+		vm:            goja.New(),
 	}
 	go re.runEventLoop()
-	re.Set("loadScript", re.loadScript)
+	re.Set("loadScript", MakeCallback(re.vm, re.loadScript))
 	re.Set("inspect", re.prettyPrintJS)
 	return re
 }
@@ -99,21 +100,20 @@ func randomSource() *rand.Rand {
 // serialized way and calls timer callback functions at the appropriate time.
 
 // Exported functions always access the vm through the event queue. You can
-// call the functions of the otto vm directly to circumvent the queue. These
+// call the functions of the goja vm directly to circumvent the queue. These
 // functions should be used if and only if running a routine that was already
 // called from JS through an RPC call.
 func (re *JSRE) runEventLoop() {
 	defer close(re.closed)
 
-	vm := otto.New()
 	r := randomSource()
-	vm.SetRandomSource(r.Float64)
+	re.vm.SetRandSource(r.Float64)
 
 	registry := map[*jsTimer]*jsTimer{}
 	ready := make(chan *jsTimer)
 
-	newTimer := func(call otto.FunctionCall, interval bool) (*jsTimer, otto.Value) {
-		delay, _ := call.Argument(1).ToInteger()
+	newTimer := func(call goja.FunctionCall, interval bool) (*jsTimer, goja.Value) {
+		delay := call.Argument(1).ToInteger()
 		if 0 >= delay {
 			delay = 1
 		}
@@ -128,47 +128,43 @@ func (re *JSRE) runEventLoop() {
 			ready <- timer
 		})
 
-		value, err := call.Otto.ToValue(timer)
-		if err != nil {
-			panic(err)
-		}
-		return timer, value
+		return timer, re.vm.ToValue(timer)
 	}
 
-	setTimeout := func(call otto.FunctionCall) otto.Value {
+	setTimeout := func(call goja.FunctionCall) goja.Value {
 		_, value := newTimer(call, false)
 		return value
 	}
 
-	setInterval := func(call otto.FunctionCall) otto.Value {
+	setInterval := func(call goja.FunctionCall) goja.Value {
 		_, value := newTimer(call, true)
 		return value
 	}
 
-	clearTimeout := func(call otto.FunctionCall) otto.Value {
-		timer, _ := call.Argument(0).Export()
+	clearTimeout := func(call goja.FunctionCall) goja.Value {
+		timer := call.Argument(0).Export()
 		if timer, ok := timer.(*jsTimer); ok {
 			timer.timer.Stop()
 			delete(registry, timer)
 		}
-		return otto.UndefinedValue()
+		return goja.Undefined()
 	}
-	vm.Set("_setTimeout", setTimeout)
-	vm.Set("_setInterval", setInterval)
-	vm.Run(`var setTimeout = function(args) {
+	re.vm.Set("_setTimeout", setTimeout)
+	re.vm.Set("_setInterval", setInterval)
+	re.vm.RunString(`var setTimeout = function(args) {
 		if (arguments.length < 1) {
 			throw TypeError("Failed to execute 'setTimeout': 1 argument required, but only 0 present.");
 		}
 		return _setTimeout.apply(this, arguments);
 	}`)
-	vm.Run(`var setInterval = function(args) {
+	re.vm.RunString(`var setInterval = function(args) {
 		if (arguments.length < 1) {
 			throw TypeError("Failed to execute 'setInterval': 1 argument required, but only 0 present.");
 		}
 		return _setInterval.apply(this, arguments);
 	}`)
-	vm.Set("clearTimeout", clearTimeout)
-	vm.Set("clearInterval", clearTimeout)
+	re.vm.Set("clearTimeout", clearTimeout)
+	re.vm.Set("clearInterval", clearTimeout)
 
 	var waitForCallbacks bool
 
@@ -178,8 +174,8 @@ loop:
 		case timer := <-ready:
 			// execute callback, remove/reschedule the timer
 			var arguments []interface{}
-			if len(timer.call.ArgumentList) > 2 {
-				tmp := timer.call.ArgumentList[2:]
+			if len(timer.call.Arguments) > 2 {
+				tmp := timer.call.Arguments[2:]
 				arguments = make([]interface{}, 2+len(tmp))
 				for i, value := range tmp {
 					arguments[i+2] = value
@@ -187,11 +183,12 @@ loop:
 			} else {
 				arguments = make([]interface{}, 1)
 			}
-			arguments[0] = timer.call.ArgumentList[0]
-			_, err := vm.Call(`Function.call.call`, nil, arguments...)
-			if err != nil {
-				fmt.Println("js error:", err, arguments)
+			arguments[0] = timer.call.Arguments[0]
+			call, isFunc := goja.AssertFunction(timer.call.Arguments[0])
+			if !isFunc {
+				panic(re.vm.ToValue("js error: timer/timeout callback is not a function"))
 			}
+			call(goja.Null(), timer.call.Arguments...)
 
 			_, inreg := registry[timer] // when clearInterval is called from within the callback don't reset it
 			if timer.interval && inreg {
@@ -204,7 +201,7 @@ loop:
 			}
 		case req := <-re.evalQueue:
 			// run the code, send the result back
-			req.fn(vm)
+			req.fn(re.vm)
 			close(req.done)
 			if waitForCallbacks && (len(registry) == 0) {
 				break loop
@@ -223,7 +220,7 @@ loop:
 }
 
 // Do executes the given function on the JS event loop.
-func (re *JSRE) Do(fn func(*otto.Otto)) {
+func (re *JSRE) Do(fn func(*goja.Runtime)) {
 	done := make(chan bool)
 	req := &evalReq{fn, done}
 	re.evalQueue <- req
@@ -246,70 +243,36 @@ func (re *JSRE) Exec(file string) error {
 	if err != nil {
 		return err
 	}
-	var script *otto.Script
-	re.Do(func(vm *otto.Otto) {
-		script, err = vm.Compile(file, code)
-		if err != nil {
-			return
-		}
-		_, err = vm.Run(script)
-	})
-	return err
-}
-
-// Bind assigns value v to a variable in the JS environment
-// This method is deprecated, use Set.
-func (re *JSRE) Bind(name string, v interface{}) error {
-	return re.Set(name, v)
+	return re.Compile(file, string(code))
 }
 
 // Run runs a piece of JS code.
-func (re *JSRE) Run(code string) (v otto.Value, err error) {
-	re.Do(func(vm *otto.Otto) { v, err = vm.Run(code) })
-	return v, err
-}
-
-// Get returns the value of a variable in the JS environment.
-func (re *JSRE) Get(ns string) (v otto.Value, err error) {
-	re.Do(func(vm *otto.Otto) { v, err = vm.Get(ns) })
+func (re *JSRE) Run(code string) (v goja.Value, err error) {
+	re.Do(func(vm *goja.Runtime) { v, err = vm.RunString(code) })
 	return v, err
 }
 
 // Set assigns value v to a variable in the JS environment.
 func (re *JSRE) Set(ns string, v interface{}) (err error) {
-	re.Do(func(vm *otto.Otto) { err = vm.Set(ns, v) })
+	re.Do(func(vm *goja.Runtime) { vm.Set(ns, v) })
 	return err
 }
 
-// loadScript executes a JS script from inside the currently executing JS code.
-func (re *JSRE) loadScript(call otto.FunctionCall) otto.Value {
-	file, err := call.Argument(0).ToString()
-	if err != nil {
-		// TODO: throw exception
-		return otto.FalseValue()
-	}
-	file = common.AbsolutePath(re.assetPath, file)
-	source, err := ioutil.ReadFile(file)
-	if err != nil {
-		// TODO: throw exception
-		return otto.FalseValue()
-	}
-	if _, err := compileAndRun(call.Otto, file, source); err != nil {
-		// TODO: throw exception
-		fmt.Println("err:", err)
-		return otto.FalseValue()
-	}
-	// TODO: return evaluation result
-	return otto.TrueValue()
+// MakeCallback turns the given function into a function that's callable by JS.
+func MakeCallback(vm *goja.Runtime, fn func(Call) (goja.Value, error)) goja.Value {
+	return vm.ToValue(func(call goja.FunctionCall) goja.Value {
+		result, err := fn(Call{call, vm})
+		if err != nil {
+			panic(vm.NewGoError(err))
+		}
+		return result
+	})
 }
 
-// Evaluate executes code and pretty prints the result to the specified output
-// stream.
-func (re *JSRE) Evaluate(code string, w io.Writer) error {
-	var fail error
-
-	re.Do(func(vm *otto.Otto) {
-		val, err := vm.Run(code)
+// Evaluate executes code and pretty prints the result to the specified output stream.
+func (re *JSRE) Evaluate(code string, w io.Writer) {
+	re.Do(func(vm *goja.Runtime) {
+		val, err := vm.RunString(code)
 		if err != nil {
 			prettyError(vm, err, w)
 		} else {
@@ -317,19 +280,33 @@ func (re *JSRE) Evaluate(code string, w io.Writer) error {
 		}
 		fmt.Fprintln(w)
 	})
-	return fail
 }
 
 // Compile compiles and then runs a piece of JS code.
-func (re *JSRE) Compile(filename string, src interface{}) (err error) {
-	re.Do(func(vm *otto.Otto) { _, err = compileAndRun(vm, filename, src) })
+func (re *JSRE) Compile(filename string, src string) (err error) {
+	re.Do(func(vm *goja.Runtime) { _, err = compileAndRun(vm, filename, src) })
 	return err
 }
 
-func compileAndRun(vm *otto.Otto, filename string, src interface{}) (otto.Value, error) {
-	script, err := vm.Compile(filename, src)
+// loadScript loads and executes a JS file.
+func (re *JSRE) loadScript(call Call) (goja.Value, error) {
+	file := call.Argument(0).ToString().String()
+	file = common.AbsolutePath(re.assetPath, file)
+	source, err := ioutil.ReadFile(file)
+	if err != nil {
+		return nil, fmt.Errorf("Could not read file %s: %v", file, err)
+	}
+	value, err := compileAndRun(re.vm, file, string(source))
+	if err != nil {
+		return nil, fmt.Errorf("Error while compiling or running script: %v", err)
+	}
+	return value, nil
+}
+
+func compileAndRun(vm *goja.Runtime, filename string, src string) (goja.Value, error) {
+	script, err := goja.Compile(filename, src, false)
 	if err != nil {
-		return otto.Value{}, err
+		return goja.Null(), err
 	}
-	return vm.Run(script)
+	return vm.RunProgram(script)
 }
diff --git a/internal/jsre/jsre_test.go b/internal/jsre/jsre_test.go
index bcb6e0dd2..bc38f7a44 100644
--- a/internal/jsre/jsre_test.go
+++ b/internal/jsre/jsre_test.go
@@ -20,25 +20,24 @@ import (
 	"io/ioutil"
 	"os"
 	"path"
+	"reflect"
 	"testing"
 	"time"
 
-	"github.com/robertkrimen/otto"
+	"github.com/dop251/goja"
 )
 
-type testNativeObjectBinding struct{}
+type testNativeObjectBinding struct {
+	vm *goja.Runtime
+}
 
 type msg struct {
 	Msg string
 }
 
-func (no *testNativeObjectBinding) TestMethod(call otto.FunctionCall) otto.Value {
-	m, err := call.Argument(0).ToString()
-	if err != nil {
-		return otto.UndefinedValue()
-	}
-	v, _ := call.Otto.ToValue(&msg{m})
-	return v
+func (no *testNativeObjectBinding) TestMethod(call goja.FunctionCall) goja.Value {
+	m := call.Argument(0).ToString().String()
+	return no.vm.ToValue(&msg{m})
 }
 
 func newWithTestJS(t *testing.T, testjs string) (*JSRE, string) {
@@ -51,7 +50,8 @@ func newWithTestJS(t *testing.T, testjs string) (*JSRE, string) {
 			t.Fatal("cannot create test.js:", err)
 		}
 	}
-	return New(dir, os.Stdout), dir
+	jsre := New(dir, os.Stdout)
+	return jsre, dir
 }
 
 func TestExec(t *testing.T) {
@@ -66,11 +66,11 @@ func TestExec(t *testing.T) {
 	if err != nil {
 		t.Errorf("expected no error, got %v", err)
 	}
-	if !val.IsString() {
+	if val.ExportType().Kind() != reflect.String {
 		t.Errorf("expected string value, got %v", val)
 	}
 	exp := "testMsg"
-	got, _ := val.ToString()
+	got := val.ToString().String()
 	if exp != got {
 		t.Errorf("expected '%v', got '%v'", exp, got)
 	}
@@ -90,11 +90,11 @@ func TestNatto(t *testing.T) {
 	if err != nil {
 		t.Errorf("expected no error, got %v", err)
 	}
-	if !val.IsString() {
+	if val.ExportType().Kind() != reflect.String {
 		t.Errorf("expected string value, got %v", val)
 	}
 	exp := "testMsg"
-	got, _ := val.ToString()
+	got := val.ToString().String()
 	if exp != got {
 		t.Errorf("expected '%v', got '%v'", exp, got)
 	}
@@ -105,7 +105,7 @@ func TestBind(t *testing.T) {
 	jsre := New("", os.Stdout)
 	defer jsre.Stop(false)
 
-	jsre.Bind("no", &testNativeObjectBinding{})
+	jsre.Set("no", &testNativeObjectBinding{vm: jsre.vm})
 
 	_, err := jsre.Run(`no.TestMethod("testMsg")`)
 	if err != nil {
@@ -125,11 +125,11 @@ func TestLoadScript(t *testing.T) {
 	if err != nil {
 		t.Errorf("expected no error, got %v", err)
 	}
-	if !val.IsString() {
+	if val.ExportType().Kind() != reflect.String {
 		t.Errorf("expected string value, got %v", val)
 	}
 	exp := "testMsg"
-	got, _ := val.ToString()
+	got := val.ToString().String()
 	if exp != got {
 		t.Errorf("expected '%v', got '%v'", exp, got)
 	}
diff --git a/internal/jsre/pretty.go b/internal/jsre/pretty.go
index 16fa91b67..4171e0090 100644
--- a/internal/jsre/pretty.go
+++ b/internal/jsre/pretty.go
@@ -19,12 +19,13 @@ package jsre
 import (
 	"fmt"
 	"io"
+	"reflect"
 	"sort"
 	"strconv"
 	"strings"
 
+	"github.com/dop251/goja"
 	"github.com/fatih/color"
-	"github.com/robertkrimen/otto"
 )
 
 const (
@@ -52,29 +53,29 @@ var boringKeys = map[string]bool{
 }
 
 // prettyPrint writes value to standard output.
-func prettyPrint(vm *otto.Otto, value otto.Value, w io.Writer) {
+func prettyPrint(vm *goja.Runtime, value goja.Value, w io.Writer) {
 	ppctx{vm: vm, w: w}.printValue(value, 0, false)
 }
 
 // prettyError writes err to standard output.
-func prettyError(vm *otto.Otto, err error, w io.Writer) {
+func prettyError(vm *goja.Runtime, err error, w io.Writer) {
 	failure := err.Error()
-	if ottoErr, ok := err.(*otto.Error); ok {
-		failure = ottoErr.String()
+	if gojaErr, ok := err.(*goja.Exception); ok {
+		failure = gojaErr.String()
 	}
 	fmt.Fprint(w, ErrorColor("%s", failure))
 }
 
-func (re *JSRE) prettyPrintJS(call otto.FunctionCall) otto.Value {
-	for _, v := range call.ArgumentList {
-		prettyPrint(call.Otto, v, re.output)
+func (re *JSRE) prettyPrintJS(call goja.FunctionCall) goja.Value {
+	for _, v := range call.Arguments {
+		prettyPrint(re.vm, v, re.output)
 		fmt.Fprintln(re.output)
 	}
-	return otto.UndefinedValue()
+	return goja.Undefined()
 }
 
 type ppctx struct {
-	vm *otto.Otto
+	vm *goja.Runtime
 	w  io.Writer
 }
 
@@ -82,35 +83,47 @@ func (ctx ppctx) indent(level int) string {
 	return strings.Repeat(indentString, level)
 }
 
-func (ctx ppctx) printValue(v otto.Value, level int, inArray bool) {
+func (ctx ppctx) printValue(v goja.Value, level int, inArray bool) {
+	if goja.IsNull(v) || goja.IsUndefined(v) {
+		fmt.Fprint(ctx.w, SpecialColor(v.String()))
+		return
+	}
+	kind := v.ExportType().Kind()
 	switch {
-	case v.IsObject():
-		ctx.printObject(v.Object(), level, inArray)
-	case v.IsNull():
-		fmt.Fprint(ctx.w, SpecialColor("null"))
-	case v.IsUndefined():
-		fmt.Fprint(ctx.w, SpecialColor("undefined"))
-	case v.IsString():
-		s, _ := v.ToString()
-		fmt.Fprint(ctx.w, StringColor("%q", s))
-	case v.IsBoolean():
-		b, _ := v.ToBoolean()
-		fmt.Fprint(ctx.w, SpecialColor("%t", b))
-	case v.IsNaN():
-		fmt.Fprint(ctx.w, NumberColor("NaN"))
-	case v.IsNumber():
-		s, _ := v.ToString()
-		fmt.Fprint(ctx.w, NumberColor("%s", s))
+	case kind == reflect.Bool:
+		fmt.Fprint(ctx.w, SpecialColor("%t", v.ToBoolean()))
+	case kind == reflect.String:
+		fmt.Fprint(ctx.w, StringColor("%q", v.String()))
+	case kind >= reflect.Int && kind <= reflect.Complex128:
+		fmt.Fprint(ctx.w, NumberColor("%s", v.String()))
 	default:
-		fmt.Fprint(ctx.w, "<unprintable>")
+		if obj, ok := v.(*goja.Object); ok {
+			ctx.printObject(obj, level, inArray)
+		} else {
+			fmt.Fprintf(ctx.w, "<unprintable %T>", v)
+		}
 	}
 }
 
-func (ctx ppctx) printObject(obj *otto.Object, level int, inArray bool) {
-	switch obj.Class() {
+// SafeGet attempt to get the value associated to `key`, and
+// catches the panic that goja creates if an error occurs in
+// key.
+func SafeGet(obj *goja.Object, key string) (ret goja.Value) {
+	defer func() {
+		if r := recover(); r != nil {
+			ret = goja.Undefined()
+		}
+	}()
+	ret = obj.Get(key)
+
+	return ret
+}
+
+func (ctx ppctx) printObject(obj *goja.Object, level int, inArray bool) {
+	switch obj.ClassName() {
 	case "Array", "GoArray":
-		lv, _ := obj.Get("length")
-		len, _ := lv.ToInteger()
+		lv := obj.Get("length")
+		len := lv.ToInteger()
 		if len == 0 {
 			fmt.Fprintf(ctx.w, "[]")
 			return
@@ -121,8 +134,8 @@ func (ctx ppctx) printObject(obj *otto.Object, level int, inArray bool) {
 		}
 		fmt.Fprint(ctx.w, "[")
 		for i := int64(0); i < len; i++ {
-			el, err := obj.Get(strconv.FormatInt(i, 10))
-			if err == nil {
+			el := obj.Get(strconv.FormatInt(i, 10))
+			if el != nil {
 				ctx.printValue(el, level+1, true)
 			}
 			if i < len-1 {
@@ -149,7 +162,7 @@ func (ctx ppctx) printObject(obj *otto.Object, level int, inArray bool) {
 		}
 		fmt.Fprintln(ctx.w, "{")
 		for i, k := range keys {
-			v, _ := obj.Get(k)
+			v := SafeGet(obj, k)
 			fmt.Fprintf(ctx.w, "%s%s: ", ctx.indent(level+1), k)
 			ctx.printValue(v, level+1, false)
 			if i < len(keys)-1 {
@@ -163,29 +176,25 @@ func (ctx ppctx) printObject(obj *otto.Object, level int, inArray bool) {
 		fmt.Fprintf(ctx.w, "%s}", ctx.indent(level))
 
 	case "Function":
-		// Use toString() to display the argument list if possible.
-		if robj, err := obj.Call("toString"); err != nil {
-			fmt.Fprint(ctx.w, FunctionColor("function()"))
-		} else {
-			desc := strings.Trim(strings.Split(robj.String(), "{")[0], " \t\n")
-			desc = strings.Replace(desc, " (", "(", 1)
-			fmt.Fprint(ctx.w, FunctionColor("%s", desc))
-		}
+		robj := obj.ToString()
+		desc := strings.Trim(strings.Split(robj.String(), "{")[0], " \t\n")
+		desc = strings.Replace(desc, " (", "(", 1)
+		fmt.Fprint(ctx.w, FunctionColor("%s", desc))
 
 	case "RegExp":
 		fmt.Fprint(ctx.w, StringColor("%s", toString(obj)))
 
 	default:
-		if v, _ := obj.Get("toString"); v.IsFunction() && level <= maxPrettyPrintLevel {
-			s, _ := obj.Call("toString")
-			fmt.Fprintf(ctx.w, "<%s %s>", obj.Class(), s.String())
+		if level <= maxPrettyPrintLevel {
+			s := obj.ToString().String()
+			fmt.Fprintf(ctx.w, "<%s %s>", obj.ClassName(), s)
 		} else {
-			fmt.Fprintf(ctx.w, "<%s>", obj.Class())
+			fmt.Fprintf(ctx.w, "<%s>", obj.ClassName())
 		}
 	}
 }
 
-func (ctx ppctx) fields(obj *otto.Object) []string {
+func (ctx ppctx) fields(obj *goja.Object) []string {
 	var (
 		vals, methods []string
 		seen          = make(map[string]bool)
@@ -195,11 +204,22 @@ func (ctx ppctx) fields(obj *otto.Object) []string {
 			return
 		}
 		seen[k] = true
-		if v, _ := obj.Get(k); v.IsFunction() {
-			methods = append(methods, k)
-		} else {
+
+		key := SafeGet(obj, k)
+		if key == nil {
+			// The value corresponding to that key could not be found
+			// (typically because it is backed by an RPC call that is
+			// not supported by this instance.  Add it to the list of
+			// values so that it appears as `undefined` to the user.
 			vals = append(vals, k)
+		} else {
+			if _, callable := goja.AssertFunction(key); callable {
+				methods = append(methods, k)
+			} else {
+				vals = append(vals, k)
+			}
 		}
+
 	}
 	iterOwnAndConstructorKeys(ctx.vm, obj, add)
 	sort.Strings(vals)
@@ -207,13 +227,13 @@ func (ctx ppctx) fields(obj *otto.Object) []string {
 	return append(vals, methods...)
 }
 
-func iterOwnAndConstructorKeys(vm *otto.Otto, obj *otto.Object, f func(string)) {
+func iterOwnAndConstructorKeys(vm *goja.Runtime, obj *goja.Object, f func(string)) {
 	seen := make(map[string]bool)
 	iterOwnKeys(vm, obj, func(prop string) {
 		seen[prop] = true
 		f(prop)
 	})
-	if cp := constructorPrototype(obj); cp != nil {
+	if cp := constructorPrototype(vm, obj); cp != nil {
 		iterOwnKeys(vm, cp, func(prop string) {
 			if !seen[prop] {
 				f(prop)
@@ -222,10 +242,17 @@ func iterOwnAndConstructorKeys(vm *otto.Otto, obj *otto.Object, f func(string))
 	}
 }
 
-func iterOwnKeys(vm *otto.Otto, obj *otto.Object, f func(string)) {
-	Object, _ := vm.Object("Object")
-	rv, _ := Object.Call("getOwnPropertyNames", obj.Value())
-	gv, _ := rv.Export()
+func iterOwnKeys(vm *goja.Runtime, obj *goja.Object, f func(string)) {
+	Object := vm.Get("Object").ToObject(vm)
+	getOwnPropertyNames, isFunc := goja.AssertFunction(Object.Get("getOwnPropertyNames"))
+	if !isFunc {
+		panic(vm.ToValue("Object.getOwnPropertyNames isn't a function"))
+	}
+	rv, err := getOwnPropertyNames(goja.Null(), obj)
+	if err != nil {
+		panic(vm.ToValue(fmt.Sprintf("Error getting object properties: %v", err)))
+	}
+	gv := rv.Export()
 	switch gv := gv.(type) {
 	case []interface{}:
 		for _, v := range gv {
@@ -240,32 +267,35 @@ func iterOwnKeys(vm *otto.Otto, obj *otto.Object, f func(string)) {
 	}
 }
 
-func (ctx ppctx) isBigNumber(v *otto.Object) bool {
+func (ctx ppctx) isBigNumber(v *goja.Object) bool {
 	// Handle numbers with custom constructor.
-	if v, _ := v.Get("constructor"); v.Object() != nil {
-		if strings.HasPrefix(toString(v.Object()), "function BigNumber") {
+	if obj := v.Get("constructor").ToObject(ctx.vm); obj != nil {
+		if strings.HasPrefix(toString(obj), "function BigNumber") {
 			return true
 		}
 	}
 	// Handle default constructor.
-	BigNumber, _ := ctx.vm.Object("BigNumber.prototype")
+	BigNumber := ctx.vm.Get("BigNumber").ToObject(ctx.vm)
 	if BigNumber == nil {
 		return false
 	}
-	bv, _ := BigNumber.Call("isPrototypeOf", v)
-	b, _ := bv.ToBoolean()
-	return b
+	prototype := BigNumber.Get("prototype").ToObject(ctx.vm)
+	isPrototypeOf, callable := goja.AssertFunction(prototype.Get("isPrototypeOf"))
+	if !callable {
+		return false
+	}
+	bv, _ := isPrototypeOf(prototype, v)
+	return bv.ToBoolean()
 }
 
-func toString(obj *otto.Object) string {
-	s, _ := obj.Call("toString")
-	return s.String()
+func toString(obj *goja.Object) string {
+	return obj.ToString().String()
 }
 
-func constructorPrototype(obj *otto.Object) *otto.Object {
-	if v, _ := obj.Get("constructor"); v.Object() != nil {
-		if v, _ = v.Object().Get("prototype"); v.Object() != nil {
-			return v.Object()
+func constructorPrototype(vm *goja.Runtime, obj *goja.Object) *goja.Object {
+	if v := obj.Get("constructor"); v != nil {
+		if v := v.ToObject(vm).Get("prototype"); v != nil {
+			return v.ToObject(vm)
 		}
 	}
 	return nil
-- 
GitLab