github.com/klaytn/klaytn@v1.12.1/console/bridge.go (about)

     1  // Modifications Copyright 2018 The klaytn Authors
     2  // Copyright 2016 The go-ethereum Authors
     3  // This file is part of the go-ethereum library.
     4  //
     5  // The go-ethereum library is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Lesser General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // The go-ethereum library is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    13  // GNU Lesser General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Lesser General Public License
    16  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    17  //
    18  // This file is derived from console/bridge.go (2018/06/04).
    19  // Modified and improved for the klaytn development.
    20  
    21  package console
    22  
    23  import (
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"reflect"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/dop251/goja"
    33  	"github.com/klaytn/klaytn/common/hexutil"
    34  	"github.com/klaytn/klaytn/console/jsre"
    35  	"github.com/klaytn/klaytn/networks/rpc"
    36  )
    37  
    38  // bridge is a collection of JavaScript utility methods to bride the .js runtime
    39  // environment and the Go RPC connection backing the remote method calls.
    40  type bridge struct {
    41  	client   *rpc.Client  // RPC client to execute Klaytn requests through
    42  	prompter UserPrompter // Input prompter to allow interactive user feedback
    43  	printer  io.Writer    // Output writer to serialize any display strings to
    44  }
    45  
    46  func getJeth(vm *goja.Runtime) *goja.Object {
    47  	jeth := vm.Get("jeth")
    48  	if jeth == nil {
    49  		panic(vm.ToValue("jeth object does not exist"))
    50  	}
    51  	return jeth.ToObject(vm)
    52  }
    53  
    54  // newBridge creates a new JavaScript wrapper around an RPC client.
    55  func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer) *bridge {
    56  	return &bridge{
    57  		client:   client,
    58  		prompter: prompter,
    59  		printer:  printer,
    60  	}
    61  }
    62  
    63  // NewAccount is a wrapper around the personal.newAccount RPC method that uses a
    64  // non-echoing password prompt to acquire the passphrase and executes the original
    65  // RPC method (saved in jeth.newAccount) with it to actually execute the RPC call.
    66  func (b *bridge) NewAccount(call jsre.Call) (goja.Value, error) {
    67  	var (
    68  		password string
    69  		confirm  string
    70  		err      error
    71  	)
    72  	switch {
    73  	// No password was specified, prompt the user for it
    74  	case len(call.Arguments) == 0:
    75  		if password, err = b.prompter.PromptPassword("Passphrase: "); err != nil {
    76  			return nil, err
    77  		}
    78  		if confirm, err = b.prompter.PromptPassword("Repeat passphrase: "); err != nil {
    79  			return nil, err
    80  		}
    81  		if password != confirm {
    82  			return nil, errors.New("passwords don't match!")
    83  		}
    84  	// A single string password was specified, use that
    85  	case len(call.Arguments) == 1 && call.Argument(0).ToString() != nil:
    86  		password = call.Argument(0).ToString().String()
    87  	default:
    88  		return nil, errors.New("expected 0 or 1 string argument")
    89  	}
    90  	// Password acquired, execute the call and return
    91  	newAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("newAccount"))
    92  	if !callable {
    93  		return nil, errors.New("jeth.newAccount is not callable")
    94  	}
    95  	ret, err := newAccount(goja.Null(), call.VM.ToValue(password))
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	return ret, nil
   100  }
   101  
   102  // OpenWallet is a wrapper around personal.openWallet which can interpret and
   103  // react to certain error messages, such as the Trezor PIN matrix request.
   104  func (b *bridge) OpenWallet(call jsre.Call) (goja.Value, error) {
   105  	// Make sure we have a wallet specified to open
   106  	if call.Argument(0).ToObject(call.VM).ClassName() != "String" {
   107  		return nil, errors.New("first argument must be the wallet URL to open")
   108  	}
   109  	wallet := call.Argument(0)
   110  
   111  	var passwd goja.Value
   112  	if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
   113  		passwd = call.VM.ToValue("")
   114  	} else {
   115  		passwd = call.Argument(1)
   116  	}
   117  	// Open the wallet and return if successful in itself
   118  	openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
   119  	if !callable {
   120  		return nil, errors.New("jeth.openWallet is not callable")
   121  	}
   122  	val, err := openWallet(goja.Null(), wallet, passwd)
   123  	if err == nil {
   124  		return val, nil
   125  	}
   126  
   127  	// Trezor PIN matrix input requested, display the matrix to the user and fetch the data
   128  	fmt.Fprintf(b.printer, "Look at the device for number positions\n\n")
   129  	fmt.Fprintf(b.printer, "7 | 8 | 9\n")
   130  	fmt.Fprintf(b.printer, "--+---+--\n")
   131  	fmt.Fprintf(b.printer, "4 | 5 | 6\n")
   132  	fmt.Fprintf(b.printer, "--+---+--\n")
   133  	fmt.Fprintf(b.printer, "1 | 2 | 3\n\n")
   134  
   135  	input, err := b.prompter.PromptPassword("Please enter current PIN: ")
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	openWallet, callable = goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
   140  	if !callable {
   141  		return nil, errors.New("jeth.openWallet is not callable")
   142  	}
   143  	return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
   144  }
   145  
   146  // UnlockAccount is a wrapper around the personal.unlockAccount RPC method that
   147  // uses a non-echoing password prompt to acquire the passphrase and executes the
   148  // original RPC method (saved in jeth.unlockAccount) with it to actually execute
   149  // the RPC call.
   150  func (b *bridge) UnlockAccount(call jsre.Call) (goja.Value, error) {
   151  	if len(call.Arguments) < 1 {
   152  		return nil, errors.New("usage: unlockAccount(account, [ password, duration ])")
   153  	}
   154  
   155  	account := call.Argument(0)
   156  	// Make sure we have an account specified to unlock.
   157  	if goja.IsUndefined(account) || goja.IsNull(account) || account.ExportType().Kind() != reflect.String {
   158  		return nil, errors.New("first argument must be the account to unlock")
   159  	}
   160  
   161  	// If password is not given or is the null value, prompt the user for it.
   162  	var passwd goja.Value
   163  	if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
   164  		fmt.Fprintf(b.printer, "Unlock account %s\n", account)
   165  		input, err := b.prompter.PromptPassword("Passphrase: ")
   166  		if err != nil {
   167  			return nil, err
   168  		}
   169  		passwd = call.VM.ToValue(input)
   170  	} else {
   171  		if call.Argument(1).ExportType().Kind() != reflect.String {
   172  			return nil, errors.New("password must be a string")
   173  		}
   174  		passwd = call.Argument(1)
   175  	}
   176  
   177  	// Third argument is the duration how long the account should be unlocked.
   178  	duration := goja.Null()
   179  	if !goja.IsUndefined(call.Argument(2)) && !goja.IsNull(call.Argument(2)) {
   180  		if !isNumber(call.Argument(2)) {
   181  			return nil, errors.New("unlock duration must be a number")
   182  		}
   183  		duration = call.Argument(2)
   184  	}
   185  
   186  	// Send the request to the backend and return.
   187  	unlockAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockAccount"))
   188  	if !callable {
   189  		return nil, errors.New("jeth.unlockAccount is not callable")
   190  	}
   191  	return unlockAccount(goja.Null(), account, passwd, duration)
   192  }
   193  
   194  // passwdByPrompt returns the password from a non-echoing password prompt.
   195  func (b *bridge) passwdByPrompt(call jsre.Call, msg string) (goja.Value, error) {
   196  	fmt.Fprintln(b.printer, msg)
   197  	input, err := b.prompter.PromptPassword("Passphrase: ")
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  	return call.VM.ToValue(input), nil
   202  }
   203  
   204  // passwdByPromptWithDuration returns the password and the duration from a non-echoing password prompt.
   205  func (b *bridge) passwdByPromptWithDuration(call jsre.Call, msg string) (goja.Value, goja.Value, error) {
   206  	var (
   207  		passwd   goja.Value
   208  		duration = goja.Null()
   209  		err      error
   210  	)
   211  
   212  	switch len(call.Arguments) {
   213  	case 0:
   214  		passwd, err = b.passwdByPrompt(call, msg)
   215  		if err != nil {
   216  			return nil, nil, err
   217  		}
   218  	case 1:
   219  		if call.Argument(0).ExportType().Kind() != reflect.String {
   220  			return nil, nil, err
   221  		}
   222  		passwd = call.Argument(0)
   223  	case 2:
   224  		if call.Argument(0).ExportType().Kind() == reflect.String && isNumber(call.Argument(1)) {
   225  			passwd = call.Argument(0)
   226  			duration = call.Argument(1)
   227  		} else {
   228  			return nil, nil, err
   229  		}
   230  	default:
   231  		return nil, nil, errors.New("invalid arguments")
   232  	}
   233  	return passwd, duration, nil
   234  }
   235  
   236  // UnlockParentOperator is a wrapper around the subbridge.unlockParentOperator RPC method that
   237  // uses a non-echoing password prompt to acquire the passphrase and executes the
   238  // original RPC method (saved in jeth.unlockParentOperator) with it to actually execute
   239  // the RPC call.
   240  func (b *bridge) UnlockParentOperator(call jsre.Call) (goja.Value, error) {
   241  	passwd, duration, err := b.passwdByPromptWithDuration(call, "Unlock parent operator account")
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  
   246  	// Send the request to the backend and return
   247  	response, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockParentOperator"))
   248  	if !callable {
   249  		return nil, errors.New("jeth.unlockParentOperator is not callable")
   250  	}
   251  	return response(goja.Null(), passwd, duration)
   252  }
   253  
   254  // UnlockChildOperator is a wrapper around the subbridge.unlockChildOperator RPC method that
   255  // uses a non-echoing password prompt to acquire the passphrase and executes the
   256  // original RPC method (saved in jeth.unlockChildOperator) with it to actually execute
   257  // the RPC call.
   258  func (b *bridge) UnlockChildOperator(call jsre.Call) (goja.Value, error) {
   259  	passwd, duration, err := b.passwdByPromptWithDuration(call, "Unlock child operator account")
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  
   264  	// Send the request to the backend and return
   265  	response, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockChildOperator"))
   266  	if !callable {
   267  		return nil, errors.New("jeth.unlockChildOperator is not callable")
   268  	}
   269  	return response(goja.Null(), passwd, duration)
   270  }
   271  
   272  // Sign is a wrapper around the personal.sign RPC method that uses a non-echoing password
   273  // prompt to acquire the passphrase and executes the original RPC method (saved in
   274  // jeth.sign) with it to actually execute the RPC call.
   275  func (b *bridge) Sign(call jsre.Call) (goja.Value, error) {
   276  	if nArgs := len(call.Arguments); nArgs < 2 {
   277  		return nil, errors.New("usage: sign(message, account, [ password ])")
   278  	}
   279  	var (
   280  		message = call.Argument(0)
   281  		account = call.Argument(1)
   282  		passwd  = call.Argument(2)
   283  	)
   284  
   285  	if goja.IsUndefined(message) || message.ExportType().Kind() != reflect.String {
   286  		return nil, errors.New("first argument must be the message to sign")
   287  	}
   288  	if goja.IsUndefined(account) || account.ExportType().Kind() != reflect.String {
   289  		return nil, errors.New("second argument must be the account to sign with")
   290  	}
   291  
   292  	// if the password is not given or null ask the user and ensure password is a string
   293  	if goja.IsUndefined(passwd) || goja.IsNull(passwd) {
   294  		fmt.Fprintf(b.printer, "Give password for account %s\n", account)
   295  		input, err := b.prompter.PromptPassword("Password: ")
   296  		if err != nil {
   297  			return nil, err
   298  		}
   299  		passwd = call.VM.ToValue(input)
   300  	} else if passwd.ExportType().Kind() != reflect.String {
   301  		return nil, errors.New("third argument must be the password to unlock the account")
   302  	}
   303  
   304  	// Send the request to the backend and return
   305  	sign, callable := goja.AssertFunction(getJeth(call.VM).Get("sign"))
   306  	if !callable {
   307  		return nil, errors.New("jeth.sign is not callable")
   308  	}
   309  	return sign(goja.Null(), message, account, passwd)
   310  }
   311  
   312  // Sleep will block the console for the specified number of seconds.
   313  func (b *bridge) Sleep(call jsre.Call) (goja.Value, error) {
   314  	if nArgs := len(call.Arguments); nArgs < 1 {
   315  		return nil, errors.New("usage: sleep(<number of seconds>)")
   316  	}
   317  	sleepObj := call.Argument(0)
   318  	if goja.IsUndefined(sleepObj) || goja.IsNull(sleepObj) || !isNumber(sleepObj) {
   319  		return nil, errors.New("usage: sleep(<number of seconds>)")
   320  	}
   321  	sleep := sleepObj.ToFloat()
   322  	time.Sleep(time.Duration(sleep * float64(time.Second)))
   323  	return call.VM.ToValue(true), nil
   324  }
   325  
   326  // SleepBlocks will block the console for a specified number of new blocks optionally
   327  // until the given timeout is reached.
   328  func (b *bridge) SleepBlocks(call jsre.Call) (goja.Value, error) {
   329  	// Parse the input parameters for the sleep.
   330  	var (
   331  		blocks = int64(0)
   332  		sleep  = int64(9999999999999999) // indefinitely
   333  	)
   334  	nArgs := len(call.Arguments)
   335  	if nArgs == 0 {
   336  		return nil, errors.New("usage: sleepBlocks(<n blocks>[, max sleep in seconds])")
   337  	}
   338  	if nArgs >= 1 {
   339  		if goja.IsNull(call.Argument(0)) || goja.IsUndefined(call.Argument(0)) || !isNumber(call.Argument(0)) {
   340  			return nil, errors.New("expected number as first argument")
   341  		}
   342  		blocks = call.Argument(0).ToInteger()
   343  	}
   344  	if nArgs >= 2 {
   345  		if goja.IsNull(call.Argument(1)) || goja.IsUndefined(call.Argument(1)) || !isNumber(call.Argument(1)) {
   346  			return nil, errors.New("expected number as second argument")
   347  		}
   348  		sleep = call.Argument(1).ToInteger()
   349  	}
   350  
   351  	// Poll the current block number until either it or a timeout is reached.
   352  	deadline := time.Now().Add(time.Duration(sleep) * time.Second)
   353  	var lastNumber hexutil.Uint64
   354  	if err := b.client.Call(&lastNumber, "klay_blockNumber"); err != nil {
   355  		return nil, err
   356  	}
   357  	for time.Now().Before(deadline) {
   358  		var number hexutil.Uint64
   359  		if err := b.client.Call(&number, "klay_blockNumber"); err != nil {
   360  			return nil, err
   361  		}
   362  		if number != lastNumber {
   363  			lastNumber = number
   364  			blocks--
   365  		}
   366  		if blocks <= 0 {
   367  			break
   368  		}
   369  		time.Sleep(time.Second)
   370  	}
   371  	return call.VM.ToValue(true), nil
   372  }
   373  
   374  type jsonrpcCall struct {
   375  	ID     int64
   376  	Method string
   377  	Params []interface{}
   378  }
   379  
   380  // Send implements the web3 provider "send" method.
   381  func (b *bridge) Send(call jsre.Call) (goja.Value, error) {
   382  	// Remarshal the request into a Go value.
   383  	reqVal, err := call.Argument(0).ToObject(call.VM).MarshalJSON()
   384  	if err != nil {
   385  		return nil, err
   386  	}
   387  
   388  	var (
   389  		rawReq = string(reqVal)
   390  		dec    = json.NewDecoder(strings.NewReader(rawReq))
   391  		reqs   []jsonrpcCall
   392  		batch  bool
   393  		resps  []*goja.Object
   394  	)
   395  	dec.UseNumber() // avoid float64s
   396  	if rawReq[0] == '[' {
   397  		batch = true
   398  		dec.Decode(&reqs)
   399  	} else {
   400  		batch = false
   401  		reqs = make([]jsonrpcCall, 1)
   402  		dec.Decode(&reqs[0])
   403  	}
   404  
   405  	// Execute the requests.
   406  	for _, req := range reqs {
   407  		resp := call.VM.NewObject()
   408  		resp.Set("jsonrpc", "2.0")
   409  		resp.Set("id", req.ID)
   410  
   411  		var result json.RawMessage
   412  		if err = b.client.Call(&result, req.Method, req.Params...); err == nil {
   413  			if result == nil {
   414  				// Special case null because it is decoded as an empty
   415  				// raw message for some reason.
   416  				resp.Set("result", goja.Null())
   417  			} else {
   418  				JSON := call.VM.Get("JSON").ToObject(call.VM)
   419  				parse, callable := goja.AssertFunction(JSON.Get("parse"))
   420  				if !callable {
   421  					return nil, errors.New("JSON.parse is not a function")
   422  				}
   423  				resultVal, err := parse(goja.Null(), call.VM.ToValue(string(result)))
   424  				if err != nil {
   425  					setError(resp, -32603, err.Error(), nil)
   426  				} else {
   427  					resp.Set("result", resultVal)
   428  				}
   429  			}
   430  		} else {
   431  			code := -32603
   432  			var data interface{}
   433  			if err, ok := err.(rpc.Error); ok {
   434  				code = err.ErrorCode()
   435  			}
   436  			if err, ok := err.(rpc.DataError); ok {
   437  				data = err.ErrorData()
   438  			}
   439  			setError(resp, code, err.Error(), data)
   440  		}
   441  		resps = append(resps, resp)
   442  	}
   443  	// Return the responses either to the callback (if supplied)
   444  	// or directly as the return value.
   445  	var result goja.Value
   446  	if batch {
   447  		result = call.VM.ToValue(resps)
   448  	} else {
   449  		result = resps[0]
   450  	}
   451  	if fn, isFunc := goja.AssertFunction(call.Argument(1)); isFunc {
   452  		fn(goja.Null(), goja.Null(), result)
   453  		return goja.Undefined(), nil
   454  	}
   455  	return result, nil
   456  }
   457  
   458  func setError(resp *goja.Object, code int, msg string, data interface{}) {
   459  	err := make(map[string]interface{})
   460  	err["code"] = code
   461  	err["message"] = msg
   462  	if data != nil {
   463  		err["data"] = data
   464  	}
   465  	resp.Set("error", err)
   466  }
   467  
   468  // isNumber returns true if input value is a JS number.
   469  func isNumber(v goja.Value) bool {
   470  	k := v.ExportType().Kind()
   471  	return k >= reflect.Int && k <= reflect.Float64
   472  }
   473  
   474  func getObject(vm *goja.Runtime, name string) *goja.Object {
   475  	v := vm.Get(name)
   476  	if v == nil {
   477  		return nil
   478  	}
   479  	return v.ToObject(vm)
   480  }