console: port to JSRE changes

Also improve a few minor details in the bridge. We can now
sleep for sub-second amounts, for example.
This commit is contained in:
Felix Lange 2020-01-22 01:59:37 +01:00
parent fd422db34f
commit 87e0aa707f
2 changed files with 244 additions and 271 deletions

View file

@ -27,6 +27,8 @@ import (
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/ethereum/go-ethereum/accounts/scwallet" "github.com/ethereum/go-ethereum/accounts/scwallet"
"github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/internal/jsre"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
) )
@ -36,41 +38,29 @@ type bridge struct {
client *rpc.Client // RPC client to execute Ethereum requests through client *rpc.Client // RPC client to execute Ethereum requests through
prompter UserPrompter // Input prompter to allow interactive user feedback prompter UserPrompter // Input prompter to allow interactive user feedback
printer io.Writer // Output writer to serialize any display strings to 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. // newBridge creates a new JavaScript wrapper around an RPC client.
func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer, runtime *goja.Runtime) *bridge { func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer) *bridge {
return &bridge{ return &bridge{
client: client, client: client,
prompter: prompter, prompter: prompter,
printer: printer, printer: printer,
runtime: runtime,
} }
} }
// IsNumber returns `true` if input value `v` is a number. func getJeth(vm *goja.Runtime) *goja.Object {
func IsNumber(v goja.Value) bool { jeth := vm.Get("jeth")
switch v.ExportType().Kind() { if jeth == nil {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: panic(vm.ToValue("jeth object does not exist"))
return true
default:
return false
} }
} return jeth.ToObject(vm)
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 // 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 // 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. // RPC method (saved in jeth.newAccount) with it to actually execute the RPC call.
func (b *bridge) NewAccount(call goja.FunctionCall) (response goja.Value) { func (b *bridge) NewAccount(call jsre.Call) (goja.Value, error) {
var ( var (
password string password string
confirm string confirm string
@ -80,58 +70,55 @@ func (b *bridge) NewAccount(call goja.FunctionCall) (response goja.Value) {
// No password was specified, prompt the user for it // No password was specified, prompt the user for it
case len(call.Arguments) == 0: case len(call.Arguments) == 0:
if password, err = b.prompter.PromptPassword("Passphrase: "); err != nil { if password, err = b.prompter.PromptPassword("Passphrase: "); err != nil {
throwJSException(b.runtime, err.Error()) return nil, err
} }
if confirm, err = b.prompter.PromptPassword("Repeat passphrase: "); err != nil { if confirm, err = b.prompter.PromptPassword("Repeat passphrase: "); err != nil {
throwJSException(b.runtime, err.Error()) return nil, err
} }
if password != confirm { if password != confirm {
throwJSException(b.runtime, "passwords don't match!") return nil, fmt.Errorf("passwords don't match!")
} }
// A single string password was specified, use that // A single string password was specified, use that
case len(call.Arguments) == 1 && call.Argument(0).ToString() != nil: case len(call.Arguments) == 1 && call.Argument(0).ToString() != nil:
password = call.Argument(0).ToString().String() password = call.Argument(0).ToString().String()
// Otherwise fail with some error
default: default:
throwJSException(b.runtime, "expected 0 or 1 string argument") return nil, fmt.Errorf("expected 0 or 1 string argument")
} }
// Password acquired, execute the call and return // Password acquired, execute the call and return
newAccount, callable := goja.AssertFunction(getJeth(b.runtime).Get("newAccount")) newAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("newAccount"))
if !callable { if !callable {
panic(b.runtime.ToValue("jeth.newAccount isn't callable")) return nil, fmt.Errorf("jeth.newAccount is not callable")
} }
ret, err := newAccount(goja.Null(), b.runtime.ToValue(password)) ret, err := newAccount(goja.Null(), call.VM.ToValue(password))
if err != nil { if err != nil {
throwJSException(b.runtime, err.Error()) return nil, err
} }
return ret return ret, nil
} }
// OpenWallet is a wrapper around personal.openWallet which can interpret and // OpenWallet is a wrapper around personal.openWallet which can interpret and
// react to certain error messages, such as the Trezor PIN matrix request. // react to certain error messages, such as the Trezor PIN matrix request.
func (b *bridge) OpenWallet(call goja.FunctionCall) (response goja.Value) { func (b *bridge) OpenWallet(call jsre.Call) (goja.Value, error) {
// Make sure we have a wallet specified to open // Make sure we have a wallet specified to open
if call.Argument(0).ToObject(b.runtime).ClassName() != "String" { if call.Argument(0).ToObject(call.VM).ClassName() != "String" {
throwJSException(b.runtime, b.runtime.ToValue("first argument must be the wallet URL to open")) return nil, fmt.Errorf("first argument must be the wallet URL to open")
} }
wallet := call.Argument(0) wallet := call.Argument(0)
var passwd goja.Value var passwd goja.Value
if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) { if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
passwd = b.runtime.ToValue("") passwd = call.VM.ToValue("")
} else { } else {
passwd = call.Argument(1) passwd = call.Argument(1)
} }
// Open the wallet and return if successful in itself // Open the wallet and return if successful in itself
openWallet, callable := goja.AssertFunction(getJeth(b.runtime).Get("openWallet")) openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
if !callable { if !callable {
throwJSException(b.runtime, b.runtime.ToValue("jeth.openWallet is not callable")) return nil, fmt.Errorf("jeth.openWallet is not callable")
} }
val, err := openWallet(goja.Null(), wallet, passwd) val, err := openWallet(goja.Null(), wallet, passwd)
if err == nil { if err == nil {
return val return val, nil
} }
// Wallet open failed, report error unless it's a PIN or PUK entry // Wallet open failed, report error unless it's a PIN or PUK entry
@ -139,32 +126,31 @@ func (b *bridge) OpenWallet(call goja.FunctionCall) (response goja.Value) {
case strings.HasSuffix(err.Error(), usbwallet.ErrTrezorPINNeeded.Error()): case strings.HasSuffix(err.Error(), usbwallet.ErrTrezorPINNeeded.Error()):
val, err = b.readPinAndReopenWallet(call) val, err = b.readPinAndReopenWallet(call)
if err == nil { if err == nil {
return val return val, nil
} }
val, err = b.readPassphraseAndReopenWallet(call) val, err = b.readPassphraseAndReopenWallet(call)
if err != nil { if err != nil {
throwJSException(b.runtime, err.Error()) return nil, err
} }
case strings.HasSuffix(err.Error(), scwallet.ErrPairingPasswordNeeded.Error()): case strings.HasSuffix(err.Error(), scwallet.ErrPairingPasswordNeeded.Error()):
// PUK input requested, fetch from the user and call open again // PUK input requested, fetch from the user and call open again
if input, err := b.prompter.PromptPassword("Please enter the pairing password: "); err != nil { input, err := b.prompter.PromptPassword("Please enter the pairing password: ")
throwJSException(b.runtime, err.Error()) if err != nil {
} else { return nil, err
passwd = b.runtime.ToValue(input)
} }
passwd = call.VM.ToValue(input)
if val, err = openWallet(goja.Null(), wallet, passwd); err != nil { if val, err = openWallet(goja.Null(), wallet, passwd); err != nil {
if !strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()) { if !strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()) {
throwJSException(b.runtime, err.Error()) return nil, err
} else { } else {
// PIN input requested, fetch from the user and call open again // PIN input requested, fetch from the user and call open again
if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { input, err := b.prompter.PromptPassword("Please enter current PIN: ")
throwJSException(b.runtime, err.Error()) if err != nil {
} else { return nil, err
passwd = b.runtime.ToValue(input)
} }
if val, err = openWallet(goja.Null(), wallet, passwd); err != nil { if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(input)); err != nil {
throwJSException(b.runtime, err.Error()) return nil, err
} }
} }
} }
@ -172,56 +158,52 @@ func (b *bridge) OpenWallet(call goja.FunctionCall) (response goja.Value) {
case strings.HasSuffix(err.Error(), scwallet.ErrPINUnblockNeeded.Error()): case strings.HasSuffix(err.Error(), scwallet.ErrPINUnblockNeeded.Error()):
// PIN unblock requested, fetch PUK and new PIN from the user // PIN unblock requested, fetch PUK and new PIN from the user
var pukpin string var pukpin string
if input, err := b.prompter.PromptPassword("Please enter current PUK: "); err != nil { input, err := b.prompter.PromptPassword("Please enter current PUK: ")
throwJSException(b.runtime, err.Error()) if err != nil {
} else { return nil, err
}
pukpin = input pukpin = input
input, err = b.prompter.PromptPassword("Please enter new PIN: ")
if err != nil {
return nil, err
} }
if input, err := b.prompter.PromptPassword("Please enter new PIN: "); err != nil {
throwJSException(b.runtime, err.Error())
} else {
pukpin += input pukpin += input
}
passwd = b.runtime.ToValue(pukpin) if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(pukpin)); err != nil {
if val, err = openWallet(goja.Null(), wallet, passwd); err != nil { return nil, err
throwJSException(b.runtime, err.Error())
} }
case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()): case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()):
// PIN input requested, fetch from the user and call open again // PIN input requested, fetch from the user and call open again
if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { input, err := b.prompter.PromptPassword("Please enter current PIN: ")
throwJSException(b.runtime, err.Error()) if err != nil {
} else { return nil, err
passwd = b.runtime.ToValue(input)
} }
if val, err = openWallet(goja.Null(), wallet, passwd); err != nil { if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(input)); err != nil {
throwJSException(b.runtime, err.Error()) return nil, err
} }
default: default:
// Unknown error occurred, drop to the user // Unknown error occurred, drop to the user
throwJSException(b.runtime, err.Error()) return nil, err
} }
return val return val, nil
} }
func (b *bridge) readPassphraseAndReopenWallet(call goja.FunctionCall) (goja.Value, error) { func (b *bridge) readPassphraseAndReopenWallet(call jsre.Call) (goja.Value, error) {
var passwd goja.Value
wallet := call.Argument(0) wallet := call.Argument(0)
if input, err := b.prompter.PromptPassword("Please enter your passphrase: "); err != nil { input, err := b.prompter.PromptPassword("Please enter your passphrase: ")
throwJSException(b.runtime, err.Error()) if err != nil {
} else { return nil, err
passwd = b.runtime.ToValue(input)
} }
openWallet, callable := goja.AssertFunction(getJeth(b.runtime).Get("openWallet")) openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
if !callable { if !callable {
return nil, fmt.Errorf("jeth.openWallet is not callable") return nil, fmt.Errorf("jeth.openWallet is not callable")
} }
return openWallet(goja.Null(), wallet, passwd) return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
} }
func (b *bridge) readPinAndReopenWallet(call goja.FunctionCall) (goja.Value, error) { func (b *bridge) readPinAndReopenWallet(call jsre.Call) (goja.Value, error) {
var passwd goja.Value
wallet := call.Argument(0) wallet := call.Argument(0)
// Trezor PIN matrix input requested, display the matrix to the user and fetch the data // 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") fmt.Fprintf(b.printer, "Look at the device for number positions\n\n")
@ -231,69 +213,65 @@ func (b *bridge) readPinAndReopenWallet(call goja.FunctionCall) (goja.Value, err
fmt.Fprintf(b.printer, "--+---+--\n") fmt.Fprintf(b.printer, "--+---+--\n")
fmt.Fprintf(b.printer, "1 | 2 | 3\n\n") fmt.Fprintf(b.printer, "1 | 2 | 3\n\n")
if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { input, err := b.prompter.PromptPassword("Please enter current PIN: ")
throwJSException(b.runtime, err.Error()) if err != nil {
} else { return nil, err
passwd = b.runtime.ToValue(input)
} }
openWallet, callable := goja.AssertFunction(getJeth(b.runtime).Get("openWallet")) openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
if !callable { if !callable {
return nil, fmt.Errorf("jeth.openWallet is not callable") return nil, fmt.Errorf("jeth.openWallet is not callable")
} }
return openWallet(goja.Null(), wallet, passwd) return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
} }
// UnlockAccount is a wrapper around the personal.unlockAccount RPC method that // UnlockAccount is a wrapper around the personal.unlockAccount RPC method that
// uses a non-echoing password prompt to acquire the passphrase and executes the // 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 // original RPC method (saved in jeth.unlockAccount) with it to actually execute
// the RPC call. // the RPC call.
func (b *bridge) UnlockAccount(call goja.FunctionCall) (response goja.Value) { func (b *bridge) UnlockAccount(call jsre.Call) (goja.Value, error) {
// Make sure we have an account specified to unlock // Make sure we have an account specified to unlock.
if call.Argument(0).ExportType().Kind() != reflect.String { if call.Argument(0).ExportType().Kind() != reflect.String {
throwJSException(b.runtime, "first argument must be the account to unlock") return nil, fmt.Errorf("first argument must be the account to unlock")
} }
account := call.Argument(0) account := call.Argument(0)
// If password is not given or is the null value, prompt the user for it // If password is not given or is the null value, prompt the user for it.
var passwd goja.Value var passwd goja.Value
if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) { if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
fmt.Fprintf(b.printer, "Unlock account %s\n", account) fmt.Fprintf(b.printer, "Unlock account %s\n", account)
if input, err := b.prompter.PromptPassword("Passphrase: "); err != nil { input, err := b.prompter.PromptPassword("Passphrase: ")
throwJSException(b.runtime, err.Error()) if err != nil {
} else { return nil, err
passwd = b.runtime.ToValue(input)
} }
passwd = call.VM.ToValue(input)
} else { } else {
if call.Argument(1).ExportType().Kind() != reflect.String { if call.Argument(1).ExportType().Kind() != reflect.String {
throwJSException(b.runtime, "password must be a string") return nil, fmt.Errorf("password must be a string")
} }
passwd = call.Argument(1) passwd = call.Argument(1)
} }
// Third argument is the duration how long the account must be unlocked.
// Third argument is the duration how long the account should be unlocked.
duration := goja.Null() duration := goja.Null()
if !goja.IsUndefined(call.Argument(2)) && !goja.IsNull(call.Argument(2)) { if !goja.IsUndefined(call.Argument(2)) && !goja.IsNull(call.Argument(2)) {
if !IsNumber(call.Argument(2)) { if !isNumber(call.Argument(2)) {
throwJSException(b.runtime, "unlock duration must be a number") return nil, fmt.Errorf("unlock duration must be a number")
} }
duration = call.Argument(2) duration = call.Argument(2)
} }
// Send the request to the backend and return
unlockAccount, callable := goja.AssertFunction(getJeth(b.runtime).Get("unlockAccount")) // Send the request to the backend and return.
unlockAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockAccount"))
if !callable { if !callable {
throwJSException(b.runtime, "jeth.unlockAccount is not callable") return nil, fmt.Errorf("jeth.unlockAccount is not callable")
} }
val, err := unlockAccount(goja.Null(), account, passwd, duration) return unlockAccount(goja.Null(), account, passwd, duration)
if err != nil {
throwJSException(b.runtime, err.Error())
}
return val
} }
// Sign is a wrapper around the personal.sign RPC method that uses a non-echoing password // 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 // prompt to acquire the passphrase and executes the original RPC method (saved in
// jeth.sign) with it to actually execute the RPC call. // jeth.sign) with it to actually execute the RPC call.
func (b *bridge) Sign(call goja.FunctionCall) (response goja.Value) { func (b *bridge) Sign(call jsre.Call) (goja.Value, error) {
var ( var (
message = call.Argument(0) message = call.Argument(0)
account = call.Argument(1) account = call.Argument(1)
@ -301,102 +279,88 @@ func (b *bridge) Sign(call goja.FunctionCall) (response goja.Value) {
) )
if message.ExportType().Kind() != reflect.String { if message.ExportType().Kind() != reflect.String {
throwJSException(b.runtime, "first argument must be the message to sign") return nil, fmt.Errorf("first argument must be the message to sign")
} }
if account.ExportType().Kind() != reflect.String { if account.ExportType().Kind() != reflect.String {
throwJSException(b.runtime, "second argument must be the account to sign with") 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 the password is not given or null ask the user and ensure password is a string
if goja.IsUndefined(passwd) || goja.IsNull(passwd) { if goja.IsUndefined(passwd) || goja.IsNull(passwd) {
fmt.Fprintf(b.printer, "Give password for account %s\n", account) fmt.Fprintf(b.printer, "Give password for account %s\n", account)
if input, err := b.prompter.PromptPassword("Password: "); err != nil { input, err := b.prompter.PromptPassword("Password: ")
throwJSException(b.runtime, err.Error()) if err != nil {
} else { return nil, err
passwd = b.runtime.ToValue(input)
} }
} passwd = call.VM.ToValue(input)
if passwd.ExportType().Kind() != reflect.String { } else if passwd.ExportType().Kind() != reflect.String {
throwJSException(b.runtime, "third argument must be the password to unlock the account") return nil, fmt.Errorf("third argument must be the password to unlock the account")
} }
// Send the request to the backend and return // Send the request to the backend and return
sign, callable := goja.AssertFunction(getJeth(b.runtime).Get("unlockAccount")) sign, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockAccount"))
if !callable { if !callable {
throwJSException(b.runtime, "jeth.unlockAccount is not callable") return nil, fmt.Errorf("jeth.unlockAccount is not callable")
} }
val, err := sign(goja.Null(), message, account, passwd) return sign(goja.Null(), message, account, passwd)
if err != nil {
throwJSException(b.runtime, err.Error())
}
return val
} }
// Sleep will block the console for the specified number of seconds. // Sleep will block the console for the specified number of seconds.
func (b *bridge) Sleep(call goja.FunctionCall) (response goja.Value) { func (b *bridge) Sleep(call jsre.Call) (goja.Value, error) {
if IsNumber(call.Argument(0)) { if !isNumber(call.Argument(0)) {
sleep := call.Argument(0).ToInteger() return nil, fmt.Errorf("usage: sleep(<number of seconds>)")
time.Sleep(time.Duration(sleep) * time.Second)
return b.runtime.ToValue(true)
} }
return throwJSException(b.runtime, "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 // SleepBlocks will block the console for a specified number of new blocks optionally
// until the given timeout is reached. // until the given timeout is reached.
func (b *bridge) SleepBlocks(call goja.FunctionCall) (response goja.Value) { func (b *bridge) SleepBlocks(call jsre.Call) (goja.Value, error) {
// Parse the input parameters for the sleep.
var ( var (
blocks = int64(0) blocks = int64(0)
sleep = int64(9999999999999999) // indefinitely sleep = int64(9999999999999999) // indefinitely
) )
// Parse the input parameters for the sleep
nArgs := len(call.Arguments) nArgs := len(call.Arguments)
if nArgs == 0 { if nArgs == 0 {
throwJSException(b.runtime, "usage: sleepBlocks(<n blocks>[, max sleep in seconds])") return nil, fmt.Errorf("usage: sleepBlocks(<n blocks>[, max sleep in seconds])")
} }
if nArgs >= 1 { if nArgs >= 1 {
if IsNumber(call.Argument(0)) { if !isNumber(call.Argument(0)) {
blocks = call.Argument(0).ToInteger() return nil, fmt.Errorf("expected number as first argument")
} else {
throwJSException(b.runtime, "expected number as first argument")
} }
blocks = call.Argument(0).ToInteger()
} }
if nArgs >= 2 { if nArgs >= 2 {
if IsNumber(call.Argument(1)) { if isNumber(call.Argument(1)) {
return nil, fmt.Errorf("expected number as second argument")
}
sleep = call.Argument(1).ToInteger() sleep = call.Argument(1).ToInteger()
} else {
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 {
blockNumber, isFunc := goja.AssertFunction(b.runtime.Get("eth.blockNumber"))
if !isFunc {
throwJSException(b.runtime, "eth.blockNumber isn't a function")
}
block, err := blockNumber(goja.Null())
if err != nil {
throwJSException(b.runtime, err.Error())
}
// 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
deadline := time.Now().Add(time.Duration(sleep) * time.Second)
// 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) { for time.Now().Before(deadline) {
if blockNumber() >= targetBlockNr { var number hexutil.Uint64
return b.runtime.ToValue(true) err := b.client.Call(&number, "eth_blockNumber")
if err != nil {
return nil, err
}
if number != lastNumber {
lastNumber = number
blocks--
}
if blocks <= 0 {
break
} }
time.Sleep(time.Second) time.Sleep(time.Second)
} }
return b.runtime.ToValue(false) return call.VM.ToValue(true), nil
} }
type jsonrpcCall struct { type jsonrpcCall struct {
@ -406,12 +370,13 @@ type jsonrpcCall struct {
} }
// Send implements the web3 provider "send" method. // Send implements the web3 provider "send" method.
func (b *bridge) Send(call goja.FunctionCall) (response goja.Value) { func (b *bridge) Send(call jsre.Call) (goja.Value, error) {
// Remarshal the request into a Go value. // Remarshal the request into a Go value.
reqVal, err := call.Argument(0).ToObject(b.runtime).MarshalJSON() reqVal, err := call.Argument(0).ToObject(call.VM).MarshalJSON()
if err != nil { if err != nil {
throwJSException(b.runtime, err.Error()) return nil, err
} }
var ( var (
rawReq = string(reqVal) rawReq = string(reqVal)
dec = json.NewDecoder(strings.NewReader(rawReq)) dec = json.NewDecoder(strings.NewReader(rawReq))
@ -431,9 +396,10 @@ func (b *bridge) Send(call goja.FunctionCall) (response goja.Value) {
// Execute the requests. // Execute the requests.
var resps []*goja.Object var resps []*goja.Object
for _, req := range reqs { for _, req := range reqs {
v, _ := b.runtime.RunString(`({"jsonrpc":"2.0"})`) resp := call.VM.NewObject()
resp := v.ToObject(b.runtime) resp.Set("jsonrpc", "2.0")
resp.Set("id", req.ID) resp.Set("id", req.ID)
var result json.RawMessage var result json.RawMessage
err = b.client.Call(&result, req.Method, req.Params...) err = b.client.Call(&result, req.Method, req.Params...)
switch err := err.(type) { switch err := err.(type) {
@ -443,13 +409,12 @@ func (b *bridge) Send(call goja.FunctionCall) (response goja.Value) {
// raw message for some reason. // raw message for some reason.
resp.Set("result", goja.Null()) resp.Set("result", goja.Null())
} else { } else {
JSON := b.runtime.Get("JSON").ToObject(b.runtime) JSON := call.VM.Get("JSON").ToObject(call.VM)
parse, callable := goja.AssertFunction(JSON.Get("parse")) parse, callable := goja.AssertFunction(JSON.Get("parse"))
if !callable { if !callable {
panic("JSON.parse isn't a function") return nil, fmt.Errorf("JSON.parse is not a function")
} }
resultVal, err := parse(goja.Null(), call.VM.ToValue(string(result)))
resultVal, err := parse(goja.Null(), b.runtime.ToValue(string(result)))
if err != nil { if err != nil {
setError(resp, -32603, err.Error()) setError(resp, -32603, err.Error())
} else { } else {
@ -466,24 +431,33 @@ func (b *bridge) Send(call goja.FunctionCall) (response goja.Value) {
// Return the responses either to the callback (if supplied) // Return the responses either to the callback (if supplied)
// or directly as the return value. // or directly as the return value.
var result goja.Value
if batch { if batch {
response = b.runtime.ToValue(resps) result = call.VM.ToValue(resps)
} else { } else {
response = resps[0] result = resps[0]
} }
if fn, isFunc := goja.AssertFunction(call.Argument(1)); isFunc { if fn, isFunc := goja.AssertFunction(call.Argument(1)); isFunc {
fn(goja.Null(), goja.Null(), response) fn(goja.Null(), goja.Null(), result)
return goja.Undefined() return goja.Undefined(), nil
} }
return response return result, nil
} }
func setError(resp *goja.Object, code int, msg string) { func setError(resp *goja.Object, code int, msg string) {
resp.Set("error", map[string]interface{}{"code": code, "message": msg}) resp.Set("error", map[string]interface{}{"code": code, "message": msg})
} }
// throwJSException panics on an goja.Value. The Goja VM will recover from the // isNumber returns true if input value is a JS number.
// Go panic and throw msg as a JavaScript error. func isNumber(v goja.Value) bool {
func throwJSException(runtime *goja.Runtime, msg interface{}) goja.Value { k := v.ExportType().Kind()
panic(runtime.ToValue(msg)) 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
}
return v.ToObject(vm)
} }

View file

@ -71,7 +71,6 @@ type Console struct {
histPath string // Absolute path to the console scrollback history histPath string // Absolute path to the console scrollback history
history []string // Scroll history maintained by the console history []string // Scroll history maintained by the console
printer io.Writer // Output writer to serialize any display strings to printer io.Writer // Output writer to serialize any display strings to
runtime *goja.Runtime // The javascript runtime
} }
// New initializes a JavaScript interpreted runtime environment and sets defaults // New initializes a JavaScript interpreted runtime environment and sets defaults
@ -88,14 +87,10 @@ func New(config Config) (*Console, error) {
config.Printer = colorable.NewColorableStdout() config.Printer = colorable.NewColorableStdout()
} }
// Create the JS runtime
runtime := goja.New()
// Initialize the console and return // Initialize the console and return
console := &Console{ console := &Console{
runtime: runtime,
client: config.Client, client: config.Client,
jsre: jsre.New(config.DocRoot, config.Printer, runtime), jsre: jsre.New(config.DocRoot, config.Printer),
prompt: config.Prompt, prompt: config.Prompt,
prompter: config.Prompter, prompter: config.Prompter,
printer: config.Printer, printer: config.Printer,
@ -113,32 +108,13 @@ func New(config Config) (*Console, error) {
// init retrieves the available APIs from the remote RPC provider and initializes // init retrieves the available APIs from the remote RPC provider and initializes
// the console's JavaScript namespaces based on the exposed modules. // the console's JavaScript namespaces based on the exposed modules.
func (c *Console) init(preload []string) error { func (c *Console) init(preload []string) error {
// Initialize the JavaScript <-> Go RPC bridge // Initialize the JavaScript <-> Go RPC bridge.
bridge := newBridge(c.client, c.prompter, c.printer, c.runtime) bridge := newBridge(c.client, c.prompter, c.printer)
c.jsre.Run("jeth = {};") if err := c.initWeb3(bridge); err != nil {
c.jsre.Run("console = {};") return err
}
c.jsre.Do(func(vm *goja.Runtime) { c.initConsoleObject(vm) })
jethObj := c.jsre.Get("jeth").ToObject(c.runtime)
jethObj.Set("send", bridge.Send)
jethObj.Set("sendAsync", bridge.Send)
consoleObj := c.runtime.Get("console").ToObject(c.runtime)
consoleObj.Set("log", c.consoleOutput)
consoleObj.Set("error", c.consoleOutput)
// Load all the internal utility JavaScript libraries
if err := c.jsre.Compile("bignumber.js", string(jsre.BignumberJs)); err != nil {
return fmt.Errorf("bignumber.js: %v", err)
}
if err := c.jsre.Compile("web3.js", string(jsre.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 // Load the supported APIs into the JavaScript runtime environment
apis, err := c.client.SupportedModules() apis, err := c.client.SupportedModules()
if err != nil { if err != nil {
@ -155,7 +131,7 @@ func (c *Console) init(preload []string) error {
return fmt.Errorf("%s.js: %v", api, err) return fmt.Errorf("%s.js: %v", api, err)
} }
flatten += fmt.Sprintf("var %s = web3.%s; ", api, api) flatten += fmt.Sprintf("var %s = web3.%s; ", api, api)
} else if obj, err := c.jsre.Run("web3." + api); err == nil && obj.ToObject(c.runtime) != nil { } else if _, err := c.jsre.Run("web3." + api); err == nil {
// Enable web3.js built-in extension if available. // Enable web3.js built-in extension if available.
flatten += fmt.Sprintf("var %s = web3.%s; ", api, api) flatten += fmt.Sprintf("var %s = web3.%s; ", api, api)
} }
@ -163,50 +139,12 @@ func (c *Console) init(preload []string) error {
if _, err = c.jsre.Run(flatten); err != nil { if _, err = c.jsre.Run(flatten); err != nil {
return fmt.Errorf("namespace flattening: %v", err) 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 c.jsre.Do(func(vm *goja.Runtime) {
if c.prompter != nil { c.initAdmin(vm, bridge)
// Retrieve the account management object to instrument c.initPersonal(vm, bridge)
personal := c.jsre.Get("personal") })
if personal == nil {
return fmt.Errorf("could not find personal")
}
// 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.ToObject(c.runtime); 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 := c.jsre.Get("admin") // Could be `nil`
if admin != nil {
// make sure the admin api is enabled over the interface
if obj := admin.ToObject(c.runtime); obj != nil {
obj.Set("sleepBlocks", bridge.SleepBlocks)
obj.Set("sleep", bridge.Sleep)
obj.Set("clearHistory", c.clearHistory)
}
}
// Preload any JavaScript files before starting the console // Preload any JavaScript files before starting the console
for _, path := range preload { for _, path := range preload {
if err := c.jsre.Exec(path); err != nil { if err := c.jsre.Exec(path); err != nil {
@ -217,7 +155,8 @@ func (c *Console) init(preload []string) error {
return fmt.Errorf("%s: %v", path, failure) return fmt.Errorf("%s: %v", path, failure)
} }
} }
// Configure the console's input prompter for scrollback and tab completion
// Configure the console's input prompter for history and tab completion.
if c.prompter != nil { if c.prompter != nil {
if content, err := ioutil.ReadFile(c.histPath); err != nil { if content, err := ioutil.ReadFile(c.histPath); err != nil {
c.prompter.SetHistory(nil) c.prompter.SetHistory(nil)
@ -230,6 +169,66 @@ func (c *Console) init(preload []string) error {
return nil return nil
} }
func (c *Console) initWeb3(bridge *bridge) error {
if err := c.jsre.Compile("bignumber.js", string(jsre.BignumberJs)); err != nil {
return fmt.Errorf("bignumber.js: %v", err)
}
if err := c.jsre.Compile("web3.js", string(jsre.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)
}
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
}
func (c *Console) initConsoleObject(vm *goja.Runtime) {
console := vm.NewObject()
console.Set("log", c.consoleOutput)
console.Set("error", c.consoleOutput)
vm.Set("console", console)
}
// 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)
}
}
// 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
}
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() { func (c *Console) clearHistory() {
c.history = nil c.history = nil
c.prompter.ClearHistory() c.prompter.ClearHistory()
@ -311,13 +310,13 @@ func (c *Console) Welcome() {
// Evaluate executes code and pretty prints the result to the specified output // Evaluate executes code and pretty prints the result to the specified output
// stream. // stream.
func (c *Console) Evaluate(statement string) error { func (c *Console) Evaluate(statement string) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
fmt.Fprintf(c.printer, "[native] error: %v\n", r) 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 // Interactive starts an interactive user session, where input is propted from