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 }