github.com/unicornultrafoundation/go-u2u@v1.0.0-rc1.0.20240205080301-e74a83d3fadc/console/console.go (about)

     1  // Copyright 2016 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum 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-ethereum 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-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package console
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"os"
    25  	"os/signal"
    26  	"path/filepath"
    27  	"regexp"
    28  	"sort"
    29  	"strings"
    30  	"syscall"
    31  
    32  	"github.com/dop251/goja"
    33  	"github.com/mattn/go-colorable"
    34  	"github.com/peterh/liner"
    35  	"github.com/unicornultrafoundation/go-u2u/console/prompt"
    36  	"github.com/unicornultrafoundation/go-u2u/internal/jsre"
    37  	"github.com/unicornultrafoundation/go-u2u/internal/jsre/deps"
    38  	"github.com/unicornultrafoundation/go-u2u/internal/web3ext"
    39  	"github.com/unicornultrafoundation/go-u2u/rpc"
    40  )
    41  
    42  var (
    43  	// u: unlock, s: signXX, sendXX, n: newAccount, i: importXX
    44  	passwordRegexp = regexp.MustCompile(`personal.[nusi]`)
    45  	onlyWhitespace = regexp.MustCompile(`^\s*$`)
    46  	exit           = regexp.MustCompile(`^\s*exit\s*;*\s*$`)
    47  )
    48  
    49  // HistoryFile is the file within the data directory to store input scrollback.
    50  const HistoryFile = "history"
    51  
    52  // DefaultPrompt is the default prompt line prefix to use for user input querying.
    53  const DefaultPrompt = "> "
    54  
    55  // Config is the collection of configurations to fine tune the behavior of the
    56  // JavaScript console.
    57  type Config struct {
    58  	DataDir  string              // Data directory to store the console history at
    59  	DocRoot  string              // Filesystem path from where to load JavaScript files from
    60  	Client   *rpc.Client         // RPC client to execute Ethereum requests through
    61  	Prompt   string              // Input prompt prefix string (defaults to DefaultPrompt)
    62  	Prompter prompt.UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
    63  	Printer  io.Writer           // Output writer to serialize any display strings to (defaults to os.Stdout)
    64  	Preload  []string            // Absolute paths to JavaScript files to preload
    65  }
    66  
    67  // Console is a JavaScript interpreted runtime environment. It is a fully fledged
    68  // JavaScript console attached to a running node via an external or in-process RPC
    69  // client.
    70  type Console struct {
    71  	client   *rpc.Client         // RPC client to execute Ethereum requests through
    72  	jsre     *jsre.JSRE          // JavaScript runtime environment running the interpreter
    73  	prompt   string              // Input prompt prefix string
    74  	prompter prompt.UserPrompter // Input prompter to allow interactive user feedback
    75  	histPath string              // Absolute path to the console scrollback history
    76  	history  []string            // Scroll history maintained by the console
    77  	printer  io.Writer           // Output writer to serialize any display strings to
    78  }
    79  
    80  // New initializes a JavaScript interpreted runtime environment and sets defaults
    81  // with the config struct.
    82  func New(config Config) (*Console, error) {
    83  	// Handle unset config values gracefully
    84  	if config.Prompter == nil {
    85  		config.Prompter = prompt.Stdin
    86  	}
    87  	if config.Prompt == "" {
    88  		config.Prompt = DefaultPrompt
    89  	}
    90  	if config.Printer == nil {
    91  		config.Printer = colorable.NewColorableStdout()
    92  	}
    93  
    94  	// Initialize the console and return
    95  	console := &Console{
    96  		client:   config.Client,
    97  		jsre:     jsre.New(config.DocRoot, config.Printer),
    98  		prompt:   config.Prompt,
    99  		prompter: config.Prompter,
   100  		printer:  config.Printer,
   101  		histPath: filepath.Join(config.DataDir, HistoryFile),
   102  	}
   103  	if err := os.MkdirAll(config.DataDir, 0700); err != nil {
   104  		return nil, err
   105  	}
   106  	if err := console.init(config.Preload); err != nil {
   107  		return nil, err
   108  	}
   109  	return console, nil
   110  }
   111  
   112  // init retrieves the available APIs from the remote RPC provider and initializes
   113  // the console's JavaScript namespaces based on the exposed modules.
   114  func (c *Console) init(preload []string) error {
   115  	c.initConsoleObject()
   116  
   117  	// Initialize the JavaScript <-> Go RPC bridge.
   118  	bridge := newBridge(c.client, c.prompter, c.printer)
   119  	if err := c.initWeb3(bridge); err != nil {
   120  		return err
   121  	}
   122  	if err := c.initExtensions(); err != nil {
   123  		return err
   124  	}
   125  
   126  	// Add bridge overrides for web3.js functionality.
   127  	c.jsre.Do(func(vm *goja.Runtime) {
   128  		c.initAdmin(vm, bridge)
   129  		c.initPersonal(vm, bridge)
   130  	})
   131  
   132  	// Preload JavaScript files.
   133  	for _, path := range preload {
   134  		if err := c.jsre.Exec(path); err != nil {
   135  			failure := err.Error()
   136  			if gojaErr, ok := err.(*goja.Exception); ok {
   137  				failure = gojaErr.String()
   138  			}
   139  			return fmt.Errorf("%s: %v", path, failure)
   140  		}
   141  	}
   142  
   143  	// Configure the input prompter for history and tab completion.
   144  	if c.prompter != nil {
   145  		if content, err := ioutil.ReadFile(c.histPath); err != nil {
   146  			c.prompter.SetHistory(nil)
   147  		} else {
   148  			c.history = strings.Split(string(content), "\n")
   149  			c.prompter.SetHistory(c.history)
   150  		}
   151  		c.prompter.SetWordCompleter(c.AutoCompleteInput)
   152  	}
   153  	return nil
   154  }
   155  
   156  func (c *Console) initConsoleObject() {
   157  	c.jsre.Do(func(vm *goja.Runtime) {
   158  		console := vm.NewObject()
   159  		console.Set("log", c.consoleOutput)
   160  		console.Set("error", c.consoleOutput)
   161  		vm.Set("console", console)
   162  	})
   163  }
   164  
   165  func (c *Console) initWeb3(bridge *bridge) error {
   166  	if err := c.jsre.Compile("bignumber.js", deps.BigNumberJS); err != nil {
   167  		return fmt.Errorf("bignumber.js: %v", err)
   168  	}
   169  	if err := c.jsre.Compile("web3.js", deps.Web3JS); err != nil {
   170  		return fmt.Errorf("web3.js: %v", err)
   171  	}
   172  	if _, err := c.jsre.Run("var Web3 = require('web3');"); err != nil {
   173  		return fmt.Errorf("web3 require: %v", err)
   174  	}
   175  	var err error
   176  	c.jsre.Do(func(vm *goja.Runtime) {
   177  		transport := vm.NewObject()
   178  		transport.Set("send", jsre.MakeCallback(vm, bridge.Send))
   179  		transport.Set("sendAsync", jsre.MakeCallback(vm, bridge.Send))
   180  		vm.Set("_consoleWeb3Transport", transport)
   181  		_, err = vm.RunString("var web3 = new Web3(_consoleWeb3Transport)")
   182  	})
   183  	return err
   184  }
   185  
   186  // initExtensions loads and registers web3.js extensions.
   187  func (c *Console) initExtensions() error {
   188  	// Compute aliases from server-provided modules.
   189  	apis, err := c.client.SupportedModules()
   190  	if err != nil {
   191  		return fmt.Errorf("api modules: %v", err)
   192  	}
   193  	aliases := map[string]struct{}{"u2u": {}, "personal": {}}
   194  	for api := range apis {
   195  		if api == "web3" {
   196  			continue
   197  		}
   198  		aliases[api] = struct{}{}
   199  		if file, ok := web3ext.Modules[api]; ok {
   200  			if err = c.jsre.Compile(api+".js", file); err != nil {
   201  				return fmt.Errorf("%s.js: %v", api, err)
   202  			}
   203  		}
   204  	}
   205  
   206  	// Apply aliases.
   207  	c.jsre.Do(func(vm *goja.Runtime) {
   208  		web3 := getObject(vm, "web3")
   209  		for name := range aliases {
   210  			if v := web3.Get(name); v != nil {
   211  				vm.Set(name, v)
   212  			}
   213  		}
   214  	})
   215  	return nil
   216  }
   217  
   218  // initAdmin creates additional admin APIs implemented by the bridge.
   219  func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
   220  	if admin := getObject(vm, "admin"); admin != nil {
   221  		admin.Set("sleepBlocks", jsre.MakeCallback(vm, bridge.SleepBlocks))
   222  		admin.Set("sleep", jsre.MakeCallback(vm, bridge.Sleep))
   223  		admin.Set("clearHistory", c.clearHistory)
   224  	}
   225  }
   226  
   227  // initPersonal redirects account-related API methods through the bridge.
   228  //
   229  // If the console is in interactive mode and the 'personal' API is available, override
   230  // the openWallet, unlockAccount, newAccount and sign methods since these require user
   231  // interaction. The original web3 callbacks are stored in 'jeth'. These will be called
   232  // by the bridge after the prompt and send the original web3 request to the backend.
   233  func (c *Console) initPersonal(vm *goja.Runtime, bridge *bridge) {
   234  	personal := getObject(vm, "personal")
   235  	if personal == nil || c.prompter == nil {
   236  		return
   237  	}
   238  	jeth := vm.NewObject()
   239  	vm.Set("jeth", jeth)
   240  	jeth.Set("openWallet", personal.Get("openWallet"))
   241  	jeth.Set("unlockAccount", personal.Get("unlockAccount"))
   242  	jeth.Set("newAccount", personal.Get("newAccount"))
   243  	jeth.Set("sign", personal.Get("sign"))
   244  	personal.Set("openWallet", jsre.MakeCallback(vm, bridge.OpenWallet))
   245  	personal.Set("unlockAccount", jsre.MakeCallback(vm, bridge.UnlockAccount))
   246  	personal.Set("newAccount", jsre.MakeCallback(vm, bridge.NewAccount))
   247  	personal.Set("sign", jsre.MakeCallback(vm, bridge.Sign))
   248  }
   249  
   250  func (c *Console) clearHistory() {
   251  	c.history = nil
   252  	c.prompter.ClearHistory()
   253  	if err := os.Remove(c.histPath); err != nil {
   254  		fmt.Fprintln(c.printer, "can't delete history file:", err)
   255  	} else {
   256  		fmt.Fprintln(c.printer, "history file deleted")
   257  	}
   258  }
   259  
   260  // consoleOutput is an override for the console.log and console.error methods to
   261  // stream the output into the configured output stream instead of stdout.
   262  func (c *Console) consoleOutput(call goja.FunctionCall) goja.Value {
   263  	var output []string
   264  	for _, argument := range call.Arguments {
   265  		output = append(output, fmt.Sprintf("%v", argument))
   266  	}
   267  	fmt.Fprintln(c.printer, strings.Join(output, " "))
   268  	return goja.Null()
   269  }
   270  
   271  // AutoCompleteInput is a pre-assembled word completer to be used by the user
   272  // input prompter to provide hints to the user about the methods available.
   273  func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) {
   274  	// No completions can be provided for empty inputs
   275  	if len(line) == 0 || pos == 0 {
   276  		return "", nil, ""
   277  	}
   278  	// Chunck data to relevant part for autocompletion
   279  	// E.g. in case of nested lines eth.getBalance(eth.coinb<tab><tab>
   280  	start := pos - 1
   281  	for ; start > 0; start-- {
   282  		// Skip all methods and namespaces (i.e. including the dot)
   283  		if line[start] == '.' || (line[start] >= 'a' && line[start] <= 'z') || (line[start] >= 'A' && line[start] <= 'Z') {
   284  			continue
   285  		}
   286  		// Handle web3 in a special way (i.e. other numbers aren't auto completed)
   287  		if start >= 3 && line[start-3:start] == "web3" {
   288  			start -= 3
   289  			continue
   290  		}
   291  		// We've hit an unexpected character, autocomplete form here
   292  		start++
   293  		break
   294  	}
   295  	return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
   296  }
   297  
   298  // Welcome shows summary of current u2u instance and some metadata about the
   299  // console's available modules.
   300  func (c *Console) Welcome() {
   301  	message := "Welcome to the Hashgraph JavaScript console!\n\n"
   302  
   303  	// Print some generic u2u metadata
   304  	res, err := c.jsre.Run(`
   305  		var message = "instance: " + web3.version.node + "\n";
   306  		try {
   307  			message += "coinbase: " + u2u.coinbase + "\n";
   308  		} catch (err) {}
   309  		message += "at block: " + u2u.blockNumber + " (" + new Date(1000 * u2u.getBlock(u2u.blockNumber).timestamp) + ")\n";
   310  		try {
   311  			message += " datadir: " + admin.datadir + "\n";
   312  		} catch (err) {}
   313  		message
   314  	`)
   315  	if err == nil {
   316  		message += res.String()
   317  	}
   318  	// List all the supported modules for the user to call
   319  	if apis, err := c.client.SupportedModules(); err == nil {
   320  		modules := make([]string, 0, len(apis))
   321  		for api, version := range apis {
   322  			if api == "eth" {
   323  				continue // hide module
   324  			}
   325  			modules = append(modules, fmt.Sprintf("%s:%s", api, version))
   326  		}
   327  		sort.Strings(modules)
   328  		message += " modules: " + strings.Join(modules, " ") + "\n"
   329  	}
   330  	message += "\nTo exit, press ctrl-d"
   331  	fmt.Fprintln(c.printer, message)
   332  }
   333  
   334  // Evaluate executes code and pretty prints the result to the specified output
   335  // stream.
   336  func (c *Console) Evaluate(statement string) {
   337  	defer func() {
   338  		if r := recover(); r != nil {
   339  			fmt.Fprintf(c.printer, "[native] error: %v\n", r)
   340  		}
   341  	}()
   342  	c.jsre.Evaluate(statement, c.printer)
   343  }
   344  
   345  // Interactive starts an interactive user session, where input is prompted from
   346  // the configured user prompter.
   347  func (c *Console) Interactive() {
   348  	var (
   349  		prompt      = c.prompt             // the current prompt line (used for multi-line inputs)
   350  		indents     = 0                    // the current number of input indents (used for multi-line inputs)
   351  		input       = ""                   // the current user input
   352  		inputLine   = make(chan string, 1) // receives user input
   353  		inputErr    = make(chan error, 1)  // receives liner errors
   354  		requestLine = make(chan string)    // requests a line of input
   355  		interrupt   = make(chan os.Signal, 1)
   356  	)
   357  
   358  	// Monitor Ctrl-C. While liner does turn on the relevant terminal mode bits to avoid
   359  	// the signal, a signal can still be received for unsupported terminals. Unfortunately
   360  	// there is no way to cancel the line reader when this happens. The readLines
   361  	// goroutine will be leaked in this case.
   362  	signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
   363  	defer signal.Stop(interrupt)
   364  
   365  	// The line reader runs in a separate goroutine.
   366  	go c.readLines(inputLine, inputErr, requestLine)
   367  	defer close(requestLine)
   368  
   369  	for {
   370  		// Send the next prompt, triggering an input read.
   371  		requestLine <- prompt
   372  
   373  		select {
   374  		case <-interrupt:
   375  			fmt.Fprintln(c.printer, "caught interrupt, exiting")
   376  			return
   377  
   378  		case err := <-inputErr:
   379  			if errors.Is(err, liner.ErrPromptAborted) {
   380  				// When prompting for multi-line input, the first Ctrl-C resets
   381  				// the multi-line state.
   382  				prompt, indents, input = c.prompt, 0, ""
   383  				continue
   384  			}
   385  			return
   386  
   387  		case line := <-inputLine:
   388  			// User input was returned by the prompter, handle special cases.
   389  			if indents <= 0 && exit.MatchString(line) {
   390  				return
   391  			}
   392  			if onlyWhitespace.MatchString(line) {
   393  				continue
   394  			}
   395  			// Append the line to the input and check for multi-line interpretation.
   396  			input += line + "\n"
   397  			indents = countIndents(input)
   398  			if indents <= 0 {
   399  				prompt = c.prompt
   400  			} else {
   401  				prompt = strings.Repeat(".", indents*3) + " "
   402  			}
   403  			// If all the needed lines are present, save the command and run it.
   404  			if indents <= 0 {
   405  				if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
   406  					if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
   407  						c.history = append(c.history, command)
   408  						if c.prompter != nil {
   409  							c.prompter.AppendHistory(command)
   410  						}
   411  					}
   412  				}
   413  				c.Evaluate(input)
   414  				input = ""
   415  			}
   416  		}
   417  	}
   418  }
   419  
   420  // readLines runs in its own goroutine, prompting for input.
   421  func (c *Console) readLines(input chan<- string, errc chan<- error, prompt <-chan string) {
   422  	for p := range prompt {
   423  		line, err := c.prompter.PromptInput(p)
   424  		if err != nil {
   425  			errc <- err
   426  		} else {
   427  			input <- line
   428  		}
   429  	}
   430  }
   431  
   432  // countIndents returns the number of indentations for the given input.
   433  // In case of invalid input such as var a = } the result can be negative.
   434  func countIndents(input string) int {
   435  	var (
   436  		indents     = 0
   437  		inString    = false
   438  		strOpenChar = ' '   // keep track of the string open char to allow var str = "I'm ....";
   439  		charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def";
   440  	)
   441  
   442  	for _, c := range input {
   443  		switch c {
   444  		case '\\':
   445  			// indicate next char as escaped when in string and previous char isn't escaping this backslash
   446  			if !charEscaped && inString {
   447  				charEscaped = true
   448  			}
   449  		case '\'', '"':
   450  			if inString && !charEscaped && strOpenChar == c { // end string
   451  				inString = false
   452  			} else if !inString && !charEscaped { // begin string
   453  				inString = true
   454  				strOpenChar = c
   455  			}
   456  			charEscaped = false
   457  		case '{', '(':
   458  			if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting
   459  				indents++
   460  			}
   461  			charEscaped = false
   462  		case '}', ')':
   463  			if !inString {
   464  				indents--
   465  			}
   466  			charEscaped = false
   467  		default:
   468  			charEscaped = false
   469  		}
   470  	}
   471  
   472  	return indents
   473  }
   474  
   475  // Execute runs the JavaScript file specified as the argument.
   476  func (c *Console) Execute(path string) error {
   477  	return c.jsre.Exec(path)
   478  }
   479  
   480  // Stop cleans up the console and terminates the runtime environment.
   481  func (c *Console) Stop(graceful bool) error {
   482  	if err := ioutil.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil {
   483  		return err
   484  	}
   485  	if err := os.Chmod(c.histPath, 0600); err != nil { // Force 0600, even if it was different previously
   486  		return err
   487  	}
   488  	c.jsre.Stop(graceful)
   489  	return nil
   490  }