github.com/core-coin/go-core/v2@v2.1.9/console/bridge.go (about)

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