diff --git a/console/bridge.go b/console/bridge.go index c7a67a6850..0d4670e756 100644 --- a/console/bridge.go +++ b/console/bridge.go @@ -20,37 +20,58 @@ 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/rpc" - "github.com/robertkrimen/otto" ) // bridge is a collection of JavaScript utility methods to bride the .js runtime // environment and the Go RPC connection backing the remote method calls. type bridge struct { - client *rpc.Client // RPC client to execute Ethereum requests through - prompter UserPrompter // Input prompter to allow interactive user feedback - printer io.Writer // Output writer to serialize any display strings to + client *rpc.Client // RPC client to execute Ethereum requests through + prompter UserPrompter // Input prompter to allow interactive user feedback + printer io.Writer // Output writer to serialize any display strings to + runtime *goja.Runtime // Pointer to the JS runtime } // newBridge creates a new JavaScript wrapper around an RPC client. -func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer) *bridge { +func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer, runtime *goja.Runtime) *bridge { return &bridge{ client: client, prompter: prompter, printer: printer, + runtime: runtime, } } +// IsNumber returns `true` if input value `v` is a number. +func IsNumber(v goja.Value) bool { + switch v.ExportType().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + default: + return false + } +} + +func getJeth(r *goja.Runtime) *goja.Object { + jethObj := r.Get("jeth") + if jethObj == nil { + panic(r.ToValue("jeth object does not exist")) + } + return jethObj.ToObject(r) +} + // 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 goja.FunctionCall) (response goja.Value) { var ( password string confirm string @@ -58,50 +79,58 @@ 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 { + throwJSException(b.runtime, err.Error()) } - if confirm, err = b.prompter.PromptPassword("Repeat password: "); err != nil { - throwJSException(err.Error()) + if confirm, err = b.prompter.PromptPassword("Repeat passphrase: "); err != nil { + throwJSException(b.runtime, err.Error()) } if password != confirm { - throwJSException("passwords don't match!") + throwJSException(b.runtime, "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() + case len(call.Arguments) == 1 && call.Argument(0).ToString() != nil: + password = call.Argument(0).ToString().String() // Otherwise fail with some error default: - throwJSException("expected 0 or 1 string argument") + throwJSException(b.runtime, "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(b.runtime).Get("newAccount")) + if !callable { + panic(b.runtime.ToValue("jeth.newAccount isn't callable")) + } + ret, err := newAccount(goja.Null(), b.runtime.ToValue(password)) if err != nil { - throwJSException(err.Error()) + throwJSException(b.runtime, err.Error()) } return ret } // 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 goja.FunctionCall) (response goja.Value) { // 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(b.runtime).ClassName() != "String" { + throwJSException(b.runtime, b.runtime.ToValue("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 = b.runtime.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(b.runtime).Get("openWallet")) + if !callable { + throwJSException(b.runtime, b.runtime.ToValue("jeth.openWallet is not callable")) + } + val, err := openWallet(goja.Null(), wallet, passwd) if err == nil { return val } @@ -115,28 +144,28 @@ func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) { } val, err = b.readPassphraseAndReopenWallet(call) if err != nil { - throwJSException(err.Error()) + throwJSException(b.runtime, err.Error()) } 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()) + throwJSException(b.runtime, err.Error()) } else { - passwd, _ = otto.ToValue(input) + passwd = b.runtime.ToValue(input) } - if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { + if val, err = openWallet(goja.Null(), wallet, passwd); err != nil { if !strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()) { - throwJSException(err.Error()) + throwJSException(b.runtime, err.Error()) } 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()) + throwJSException(b.runtime, err.Error()) } else { - passwd, _ = otto.ToValue(input) + passwd = b.runtime.ToValue(input) } - if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { - throwJSException(err.Error()) + if val, err = openWallet(goja.Null(), wallet, passwd); err != nil { + throwJSException(b.runtime, err.Error()) } } } @@ -145,51 +174,55 @@ func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) { // 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()) + throwJSException(b.runtime, err.Error()) } else { pukpin = input } if input, err := b.prompter.PromptPassword("Please enter new PIN: "); err != nil { - throwJSException(err.Error()) + throwJSException(b.runtime, err.Error()) } else { pukpin += input } - passwd, _ = otto.ToValue(pukpin) - if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { - throwJSException(err.Error()) + passwd = b.runtime.ToValue(pukpin) + if val, err = openWallet(goja.Null(), wallet, passwd); err != nil { + throwJSException(b.runtime, err.Error()) } 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()) + throwJSException(b.runtime, err.Error()) } else { - passwd, _ = otto.ToValue(input) + passwd = b.runtime.ToValue(input) } - if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { - throwJSException(err.Error()) + if val, err = openWallet(goja.Null(), wallet, passwd); err != nil { + throwJSException(b.runtime, err.Error()) } default: // Unknown error occurred, drop to the user - throwJSException(err.Error()) + throwJSException(b.runtime, err.Error()) } return val } -func (b *bridge) readPassphraseAndReopenWallet(call otto.FunctionCall) (otto.Value, error) { - var passwd otto.Value +func (b *bridge) readPassphraseAndReopenWallet(call goja.FunctionCall) (goja.Value, error) { + var passwd goja.Value wallet := call.Argument(0) - if input, err := b.prompter.PromptPassword("Please enter your password: "); err != nil { - throwJSException(err.Error()) + if input, err := b.prompter.PromptPassword("Please enter your passphrase: "); err != nil { + throwJSException(b.runtime, err.Error()) } else { - passwd, _ = otto.ToValue(input) + passwd = b.runtime.ToValue(input) } - return call.Otto.Call("jeth.openWallet", nil, wallet, passwd) + openWallet, callable := goja.AssertFunction(getJeth(b.runtime).Get("openWallet")) + if !callable { + return nil, fmt.Errorf("jeth.openWallet is not callable") + } + return openWallet(goja.Null(), wallet, passwd) } -func (b *bridge) readPinAndReopenWallet(call otto.FunctionCall) (otto.Value, error) { - var passwd otto.Value +func (b *bridge) readPinAndReopenWallet(call goja.FunctionCall) (goja.Value, error) { + var passwd goja.Value 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") @@ -200,52 +233,60 @@ func (b *bridge) readPinAndReopenWallet(call otto.FunctionCall) (otto.Value, err fmt.Fprintf(b.printer, "1 | 2 | 3\n\n") if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { - throwJSException(err.Error()) + throwJSException(b.runtime, err.Error()) } else { - passwd, _ = otto.ToValue(input) + passwd = b.runtime.ToValue(input) } - return call.Otto.Call("jeth.openWallet", nil, wallet, passwd) + openWallet, callable := goja.AssertFunction(getJeth(b.runtime).Get("openWallet")) + if !callable { + return nil, fmt.Errorf("jeth.openWallet is not callable") + } + return openWallet(goja.Null(), wallet, passwd) } // 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) { +func (b *bridge) UnlockAccount(call goja.FunctionCall) (response goja.Value) { // Make sure we have an account specified to unlock - if !call.Argument(0).IsString() { - throwJSException("first argument must be the account to unlock") + if call.Argument(0).ExportType().Kind() != reflect.String { + throwJSException(b.runtime, "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 + var passwd goja.Value - if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() { + 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()) + if input, err := b.prompter.PromptPassword("Passphrase: "); err != nil { + throwJSException(b.runtime, err.Error()) } else { - passwd, _ = otto.ToValue(input) + passwd = b.runtime.ToValue(input) } } else { - if !call.Argument(1).IsString() { - throwJSException("password must be a string") + if call.Argument(1).ExportType().Kind() != reflect.String { + throwJSException(b.runtime, "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") + duration := goja.Null() + if !goja.IsUndefined(call.Argument(2)) && !goja.IsNull(call.Argument(2)) { + if !IsNumber(call.Argument(2)) { + throwJSException(b.runtime, "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) + unlockAccount, callable := goja.AssertFunction(getJeth(b.runtime).Get("unlockAccount")) + if !callable { + throwJSException(b.runtime, "jeth.unlockAccount is not callable") + } + val, err := unlockAccount(goja.Null(), account, passwd, duration) if err != nil { - throwJSException(err.Error()) + throwJSException(b.runtime, err.Error()) } return val } @@ -253,89 +294,98 @@ func (b *bridge) UnlockAccount(call otto.FunctionCall) (response otto.Value) { // 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 goja.FunctionCall) (response goja.Value) { 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 { + throwJSException(b.runtime, "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 { + throwJSException(b.runtime, "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()) + throwJSException(b.runtime, err.Error()) } else { - passwd, _ = otto.ToValue(input) + passwd = b.runtime.ToValue(input) } } - if !passwd.IsString() { - throwJSException("third argument must be the password to unlock the account") + if passwd.ExportType().Kind() != reflect.String { + throwJSException(b.runtime, "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) + sign, callable := goja.AssertFunction(getJeth(b.runtime).Get("unlockAccount")) + if !callable { + throwJSException(b.runtime, "jeth.unlockAccount is not callable") + } + val, err := sign(goja.Null(), message, account, passwd) if err != nil { - throwJSException(err.Error()) + throwJSException(b.runtime, err.Error()) } return val } // 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() +func (b *bridge) Sleep(call goja.FunctionCall) (response goja.Value) { + if IsNumber(call.Argument(0)) { + sleep := call.Argument(0).ToInteger() time.Sleep(time.Duration(sleep) * time.Second) - return otto.TrueValue() + return b.runtime.ToValue(true) } - return throwJSException("usage: sleep()") + return throwJSException(b.runtime, "usage: sleep()") } // 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 goja.FunctionCall) (response goja.Value) { 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([, max sleep in seconds])") + throwJSException(b.runtime, "usage: sleepBlocks([, max sleep in seconds])") } if nArgs >= 1 { - if call.Argument(0).IsNumber() { - blocks, _ = call.Argument(0).ToInteger() + if IsNumber(call.Argument(0)) { + blocks = call.Argument(0).ToInteger() + } else { - throwJSException("expected number as first argument") + throwJSException(b.runtime, "expected number as first argument") } } if nArgs >= 2 { - if call.Argument(1).IsNumber() { - sleep, _ = call.Argument(1).ToInteger() + if IsNumber(call.Argument(1)) { + sleep = call.Argument(1).ToInteger() } else { - throwJSException("expected number as second argument") + throwJSException(b.runtime, "expected number as second argument") } } // 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") - if err != nil { - throwJSException(err.Error()) + blockNumber, isFunc := goja.AssertFunction(b.runtime.Get("eth.blockNumber")) + if !isFunc { + throwJSException(b.runtime, "eth.blockNumber isn't a function") } - block, err := result.ToInteger() + block, err := blockNumber(goja.Null()) if err != nil { - throwJSException(err.Error()) + throwJSException(b.runtime, err.Error()) } - return block + // XXX This will return 0 if blockNumber isn't an Integer. This is + // actually consistent with the current behavior (block number is 0 + // until the sync is done) but not safe enough. + return block.ToInteger() + } // Poll the current block number until either it ot a timeout is reached targetBlockNr := blockNumber() + blocks @@ -343,11 +393,11 @@ func (b *bridge) SleepBlocks(call otto.FunctionCall) (response otto.Value) { for time.Now().Before(deadline) { if blockNumber() >= targetBlockNr { - return otto.TrueValue() + return b.runtime.ToValue(true) } time.Sleep(time.Second) } - return otto.FalseValue() + return b.runtime.ToValue(false) } type jsonrpcCall struct { @@ -357,15 +407,14 @@ 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 goja.FunctionCall) (response goja.Value) { // 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(b.runtime).MarshalJSON() if err != nil { - throwJSException(err.Error()) + throwJSException(b.runtime, err.Error()) } var ( - rawReq = reqVal.String() + rawReq = string(reqVal) dec = json.NewDecoder(strings.NewReader(rawReq)) reqs []jsonrpcCall batch bool @@ -381,9 +430,10 @@ 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"})`) + v, _ := b.runtime.RunString(`({"jsonrpc":"2.0"})`) + resp := v.ToObject(b.runtime) resp.Set("id", req.ID) var result json.RawMessage err = b.client.Call(&result, req.Method, req.Params...) @@ -392,9 +442,15 @@ 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 := b.runtime.Get("JSON").ToObject(b.runtime) + parse, callable := goja.AssertFunction(JSON.Get("parse")) + if !callable { + panic("JSON.parse isn't a function") + } + + resultVal, err := parse(goja.Null(), b.runtime.ToValue(string(result))) if err != nil { setError(resp, -32603, err.Error()) } else { @@ -406,33 +462,29 @@ 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. if batch { - response = resps.Value() + response = b.runtime.ToValue(resps) } else { - response, _ = resps.Get("0") + response = 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(), response) + return goja.Undefined() } return response } -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 +// throwJSException panics on an goja.Value. The Goja 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) - } - panic(val) +func throwJSException(runtime *goja.Runtime, msg interface{}) goja.Value { + panic(runtime.ToValue(msg)) } diff --git a/go.mod b/go.mod index e12d90f945..ad4d689793 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( 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-20191203121440-007eef3bc40f // indirect + github.com/dop251/goja v0.0.0-20191203121440-007eef3bc40f 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