cmd, console, internal: experimental console based on nodejs

This commit is contained in:
Péter Szilágyi 2024-10-22 17:47:04 +03:00
parent a5fe7353cf
commit 228a7d0f43
21 changed files with 427 additions and 2438 deletions

View file

@ -27,7 +27,7 @@ import (
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/console/prompt"
"github.com/ethereum/go-ethereum/internal/prompt"
"github.com/ethereum/go-ethereum/p2p/dnsdisc"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/urfave/cli/v2"

View file

@ -17,9 +17,6 @@
package main
import (
"fmt"
"strings"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/console"
"github.com/ethereum/go-ethereum/internal/flags"
@ -52,17 +49,6 @@ which exposes a node admin interface as well as the Ðapp JavaScript API.
See https://geth.ethereum.org/docs/interacting-with-geth/javascript-console.
This command allows to open a console on a running geth node.`,
}
javascriptCommand = &cli.Command{
Action: ephemeralConsole,
Name: "js",
Usage: "(DEPRECATED) Execute the specified JavaScript files",
ArgsUsage: "<jsfile> [jsfile...]",
Flags: flags.Merge(nodeFlags, consoleFlags),
Description: `
The JavaScript VM exposes a node admin interface as well as the Ðapp
JavaScript API. See https://geth.ethereum.org/docs/interacting-with-geth/javascript-console`,
}
)
// localConsole starts a new geth node, attaching a JavaScript console to it at the
@ -75,86 +61,11 @@ func localConsole(ctx *cli.Context) error {
defer stack.Close()
// Attach to the newly started node and create the JavaScript console.
client := stack.Attach()
config := console.Config{
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.String(utils.JSpathFlag.Name),
Client: client,
Preload: utils.MakeConsolePreloads(ctx),
}
console, err := console.New(config)
if err != nil {
return fmt.Errorf("failed to start the JavaScript console: %v", err)
}
defer console.Stop(false)
// If only a short execution was requested, evaluate and return.
if script := ctx.String(utils.ExecFlag.Name); script != "" {
console.Evaluate(script)
return nil
}
// Track node shutdown and stop the console when it goes down.
// This happens when SIGTERM is sent to the process.
go func() {
stack.Wait()
console.StopInteractive()
}()
// Print the welcome screen and enter interactive mode.
console.Welcome()
console.Interactive()
return nil
return console.RunInProc(stack.IPCEndpoint())
}
// remoteConsole will connect to a remote geth instance, attaching a JavaScript
// console to it.
func remoteConsole(ctx *cli.Context) error {
if ctx.Args().Len() > 1 {
utils.Fatalf("invalid command-line: too many arguments")
}
endpoint := ctx.Args().First()
if endpoint == "" {
cfg := defaultNodeConfig()
utils.SetDataDir(ctx, &cfg)
endpoint = cfg.IPCEndpoint()
}
client, err := utils.DialRPCWithHeaders(endpoint, ctx.StringSlice(utils.HttpHeaderFlag.Name))
if err != nil {
utils.Fatalf("Unable to attach to remote geth: %v", err)
}
config := console.Config{
DataDir: utils.MakeDataDir(ctx),
DocRoot: ctx.String(utils.JSpathFlag.Name),
Client: client,
Preload: utils.MakeConsolePreloads(ctx),
}
console, err := console.New(config)
if err != nil {
utils.Fatalf("Failed to start the JavaScript console: %v", err)
}
defer console.Stop(false)
if script := ctx.String(utils.ExecFlag.Name); script != "" {
console.Evaluate(script)
return nil
}
// Otherwise print the welcome screen and enter interactive mode
console.Welcome()
console.Interactive()
return nil
}
// ephemeralConsole starts a new geth node, attaches an ephemeral JavaScript
// console to it, executes each of the files specified as arguments and tears
// everything down.
func ephemeralConsole(ctx *cli.Context) error {
var b strings.Builder
for _, file := range ctx.Args().Slice() {
b.WriteString(fmt.Sprintf("loadScript('%s');", file))
}
utils.Fatalf(`The "js" command is deprecated. Please use the following instead:
geth --exec "%s" console`, b.String())
return nil
return console.RunAsProc(ctx.Args().First())
}

View file

@ -30,13 +30,13 @@ import (
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/console/prompt"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/internal/prompt"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"

View file

@ -29,11 +29,11 @@ import (
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/console/prompt"
"github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/internal/debug"
"github.com/ethereum/go-ethereum/internal/flags"
"github.com/ethereum/go-ethereum/internal/prompt"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/node"
@ -230,7 +230,6 @@ func init() {
// See consolecmd.go:
consoleCommand,
attachCommand,
javascriptCommand,
// See misccmd.go:
versionCommand,
versionCheckCommand,

View file

@ -20,7 +20,7 @@ package utils
import (
"fmt"
"github.com/ethereum/go-ethereum/console/prompt"
"github.com/ethereum/go-ethereum/internal/prompt"
)
// GetPassPhrase displays the given text(prompt) to the user and requests some textual

View file

@ -1,484 +0,0 @@
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package console
import (
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"strings"
"time"
"github.com/dop251/goja"
"github.com/ethereum/go-ethereum/accounts/scwallet"
"github.com/ethereum/go-ethereum/accounts/usbwallet"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/console/prompt"
"github.com/ethereum/go-ethereum/internal/jsre"
"github.com/ethereum/go-ethereum/rpc"
)
// bridge is a collection of JavaScript utility methods to bride the .js runtime
// environment and the Go RPC connection backing the remote method calls.
type bridge struct {
client *rpc.Client // RPC client to execute Ethereum requests through
prompter prompt.UserPrompter // Input prompter to allow interactive user feedback
printer io.Writer // Output writer to serialize any display strings to
}
// newBridge creates a new JavaScript wrapper around an RPC client.
func newBridge(client *rpc.Client, prompter prompt.UserPrompter, printer io.Writer) *bridge {
return &bridge{
client: client,
prompter: prompter,
printer: printer,
}
}
func getJeth(vm *goja.Runtime) *goja.Object {
jeth := vm.Get("jeth")
if jeth == nil {
panic(vm.ToValue("jeth object does not exist"))
}
return jeth.ToObject(vm)
}
// 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
// RPC method (saved in jeth.newAccount) with it to actually execute the RPC call.
func (b *bridge) NewAccount(call jsre.Call) (goja.Value, error) {
var (
password string
confirm string
err error
)
switch {
// No password was specified, prompt the user for it
case len(call.Arguments) == 0:
if password, err = b.prompter.PromptPassword("Passphrase: "); err != nil {
return nil, err
}
if confirm, err = b.prompter.PromptPassword("Repeat passphrase: "); err != nil {
return nil, err
}
if password != confirm {
return nil, errors.New("passwords don't match")
}
// A single string password was specified, use that
case len(call.Arguments) == 1 && call.Argument(0).ToString() != nil:
password = call.Argument(0).ToString().String()
default:
return nil, errors.New("expected 0 or 1 string argument")
}
// Password acquired, execute the call and return
newAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("newAccount"))
if !callable {
return nil, errors.New("jeth.newAccount is not callable")
}
ret, err := newAccount(goja.Null(), call.VM.ToValue(password))
if err != nil {
return nil, err
}
return ret, nil
}
// OpenWallet is a wrapper around personal.openWallet which can interpret and
// react to certain error messages, such as the Trezor PIN matrix request.
func (b *bridge) OpenWallet(call jsre.Call) (goja.Value, error) {
// Make sure we have a wallet specified to open
if call.Argument(0).ToObject(call.VM).ClassName() != "String" {
return nil, errors.New("first argument must be the wallet URL to open")
}
wallet := call.Argument(0)
var passwd goja.Value
if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
passwd = call.VM.ToValue("")
} else {
passwd = call.Argument(1)
}
// Open the wallet and return if successful in itself
openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
if !callable {
return nil, errors.New("jeth.openWallet is not callable")
}
val, err := openWallet(goja.Null(), wallet, passwd)
if err == nil {
return val, nil
}
// Wallet open failed, report error unless it's a PIN or PUK entry
switch {
case strings.HasSuffix(err.Error(), usbwallet.ErrTrezorPINNeeded.Error()):
val, err = b.readPinAndReopenWallet(call)
if err == nil {
return val, nil
}
val, err = b.readPassphraseAndReopenWallet(call)
if err != nil {
return nil, err
}
case strings.HasSuffix(err.Error(), scwallet.ErrPairingPasswordNeeded.Error()):
// PUK input requested, fetch from the user and call open again
input, err := b.prompter.PromptPassword("Please enter the pairing password: ")
if err != nil {
return nil, err
}
passwd = call.VM.ToValue(input)
if val, err = openWallet(goja.Null(), wallet, passwd); err != nil {
if !strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()) {
return nil, err
}
// PIN input requested, fetch from the user and call open again
input, err := b.prompter.PromptPassword("Please enter current PIN: ")
if err != nil {
return nil, err
}
if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(input)); err != nil {
return nil, err
}
}
case strings.HasSuffix(err.Error(), scwallet.ErrPINUnblockNeeded.Error()):
// PIN unblock requested, fetch PUK and new PIN from the user
var pukpin string
input, err := b.prompter.PromptPassword("Please enter current PUK: ")
if err != nil {
return nil, err
}
pukpin = input
input, err = b.prompter.PromptPassword("Please enter new PIN: ")
if err != nil {
return nil, err
}
pukpin += input
if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(pukpin)); err != nil {
return nil, err
}
case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()):
// PIN input requested, fetch from the user and call open again
input, err := b.prompter.PromptPassword("Please enter current PIN: ")
if err != nil {
return nil, err
}
if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(input)); err != nil {
return nil, err
}
default:
// Unknown error occurred, drop to the user
return nil, err
}
return val, nil
}
func (b *bridge) readPassphraseAndReopenWallet(call jsre.Call) (goja.Value, error) {
wallet := call.Argument(0)
input, err := b.prompter.PromptPassword("Please enter your passphrase: ")
if err != nil {
return nil, err
}
openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
if !callable {
return nil, errors.New("jeth.openWallet is not callable")
}
return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
}
func (b *bridge) readPinAndReopenWallet(call jsre.Call) (goja.Value, error) {
wallet := call.Argument(0)
// 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, "7 | 8 | 9\n")
fmt.Fprintf(b.printer, "--+---+--\n")
fmt.Fprintf(b.printer, "4 | 5 | 6\n")
fmt.Fprintf(b.printer, "--+---+--\n")
fmt.Fprintf(b.printer, "1 | 2 | 3\n\n")
input, err := b.prompter.PromptPassword("Please enter current PIN: ")
if err != nil {
return nil, err
}
openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
if !callable {
return nil, errors.New("jeth.openWallet is not callable")
}
return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
}
// UnlockAccount is a wrapper around the personal.unlockAccount RPC method that
// 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
// the RPC call.
func (b *bridge) UnlockAccount(call jsre.Call) (goja.Value, error) {
if len(call.Arguments) < 1 {
return nil, errors.New("usage: unlockAccount(account, [ password, duration ])")
}
account := call.Argument(0)
// Make sure we have an account specified to unlock.
if goja.IsUndefined(account) || goja.IsNull(account) || account.ExportType().Kind() != reflect.String {
return nil, errors.New("first argument must be the account to unlock")
}
// If password is not given or is the null value, prompt the user for it.
var passwd goja.Value
if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
fmt.Fprintf(b.printer, "Unlock account %s\n", account)
input, err := b.prompter.PromptPassword("Passphrase: ")
if err != nil {
return nil, err
}
passwd = call.VM.ToValue(input)
} else {
if call.Argument(1).ExportType().Kind() != reflect.String {
return nil, errors.New("password must be a string")
}
passwd = call.Argument(1)
}
// Third argument is the duration how long the account should be unlocked.
duration := goja.Null()
if !goja.IsUndefined(call.Argument(2)) && !goja.IsNull(call.Argument(2)) {
if !isNumber(call.Argument(2)) {
return nil, errors.New("unlock duration must be a number")
}
duration = call.Argument(2)
}
// Send the request to the backend and return.
unlockAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockAccount"))
if !callable {
return nil, errors.New("jeth.unlockAccount is not callable")
}
return unlockAccount(goja.Null(), account, passwd, duration)
}
// 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
// jeth.sign) with it to actually execute the RPC call.
func (b *bridge) Sign(call jsre.Call) (goja.Value, error) {
if nArgs := len(call.Arguments); nArgs < 2 {
return nil, errors.New("usage: sign(message, account, [ password ])")
}
var (
message = call.Argument(0)
account = call.Argument(1)
passwd = call.Argument(2)
)
if goja.IsUndefined(message) || message.ExportType().Kind() != reflect.String {
return nil, errors.New("first argument must be the message to sign")
}
if goja.IsUndefined(account) || account.ExportType().Kind() != reflect.String {
return nil, errors.New("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 goja.IsUndefined(passwd) || goja.IsNull(passwd) {
fmt.Fprintf(b.printer, "Give password for account %s\n", account)
input, err := b.prompter.PromptPassword("Password: ")
if err != nil {
return nil, err
}
passwd = call.VM.ToValue(input)
} else if passwd.ExportType().Kind() != reflect.String {
return nil, errors.New("third argument must be the password to unlock the account")
}
// Send the request to the backend and return
sign, callable := goja.AssertFunction(getJeth(call.VM).Get("sign"))
if !callable {
return nil, errors.New("jeth.sign is not callable")
}
return sign(goja.Null(), message, account, passwd)
}
// Sleep will block the console for the specified number of seconds.
func (b *bridge) Sleep(call jsre.Call) (goja.Value, error) {
if nArgs := len(call.Arguments); nArgs < 1 {
return nil, errors.New("usage: sleep(<number of seconds>)")
}
sleepObj := call.Argument(0)
if goja.IsUndefined(sleepObj) || goja.IsNull(sleepObj) || !isNumber(sleepObj) {
return nil, errors.New("usage: sleep(<number of seconds>)")
}
sleep := sleepObj.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
// until the given timeout is reached.
func (b *bridge) SleepBlocks(call jsre.Call) (goja.Value, error) {
// Parse the input parameters for the sleep.
var (
blocks = int64(0)
sleep = int64(9999999999999999) // indefinitely
)
nArgs := len(call.Arguments)
if nArgs == 0 {
return nil, errors.New("usage: sleepBlocks(<n blocks>[, max sleep in seconds])")
}
if nArgs >= 1 {
if goja.IsNull(call.Argument(0)) || goja.IsUndefined(call.Argument(0)) || !isNumber(call.Argument(0)) {
return nil, errors.New("expected number as first argument")
}
blocks = call.Argument(0).ToInteger()
}
if nArgs >= 2 {
if goja.IsNull(call.Argument(1)) || goja.IsUndefined(call.Argument(1)) || !isNumber(call.Argument(1)) {
return nil, errors.New("expected number as second argument")
}
sleep = call.Argument(1).ToInteger()
}
// Poll the current block number until either it or a timeout is reached.
deadline := time.Now().Add(time.Duration(sleep) * time.Second)
var lastNumber hexutil.Uint64
if err := b.client.Call(&lastNumber, "eth_blockNumber"); err != nil {
return nil, err
}
for time.Now().Before(deadline) {
var number hexutil.Uint64
if err := b.client.Call(&number, "eth_blockNumber"); err != nil {
return nil, err
}
if number != lastNumber {
lastNumber = number
blocks--
}
if blocks <= 0 {
break
}
time.Sleep(time.Second)
}
return call.VM.ToValue(true), nil
}
type jsonrpcCall struct {
ID int64
Method string
Params []interface{}
}
// Send implements the web3 provider "send" method.
func (b *bridge) Send(call jsre.Call) (goja.Value, error) {
// Remarshal the request into a Go value.
reqVal, err := call.Argument(0).ToObject(call.VM).MarshalJSON()
if err != nil {
return nil, err
}
var (
rawReq = string(reqVal)
dec = json.NewDecoder(strings.NewReader(rawReq))
reqs []jsonrpcCall
batch bool
)
dec.UseNumber() // avoid float64s
if rawReq[0] == '[' {
batch = true
dec.Decode(&reqs)
} else {
batch = false
reqs = make([]jsonrpcCall, 1)
dec.Decode(&reqs[0])
}
// Execute the requests.
var resps []*goja.Object
for _, req := range reqs {
resp := call.VM.NewObject()
resp.Set("jsonrpc", "2.0")
resp.Set("id", req.ID)
var result json.RawMessage
if err = b.client.Call(&result, req.Method, req.Params...); err == nil {
if result == nil {
// Special case null because it is decoded as an empty
// raw message for some reason.
resp.Set("result", goja.Null())
} else {
JSON := call.VM.Get("JSON").ToObject(call.VM)
parse, callable := goja.AssertFunction(JSON.Get("parse"))
if !callable {
return nil, errors.New("JSON.parse is not a function")
}
resultVal, err := parse(goja.Null(), call.VM.ToValue(string(result)))
if err != nil {
setError(resp, -32603, err.Error(), nil)
} else {
resp.Set("result", resultVal)
}
}
} else {
code := -32603
var data interface{}
if err, ok := err.(rpc.Error); ok {
code = err.ErrorCode()
}
if err, ok := err.(rpc.DataError); ok {
data = err.ErrorData()
}
setError(resp, code, err.Error(), data)
}
resps = append(resps, resp)
}
// Return the responses either to the callback (if supplied)
// or directly as the return value.
var result goja.Value
if batch {
result = call.VM.ToValue(resps)
} else {
result = resps[0]
}
if fn, isFunc := goja.AssertFunction(call.Argument(1)); isFunc {
fn(goja.Null(), goja.Null(), result)
return goja.Undefined(), nil
}
return result, nil
}
func setError(resp *goja.Object, code int, msg string, data interface{}) {
err := make(map[string]interface{})
err["code"] = code
err["message"] = msg
if data != nil {
err["data"] = data
}
resp.Set("error", err)
}
// isNumber returns true if input value is a JS number.
func isNumber(v goja.Value) bool {
k := v.ExportType().Kind()
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

@ -1,4 +1,4 @@
// Copyright 2016 The go-ethereum Authors
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
@ -17,548 +17,72 @@
package console
import (
"errors"
"fmt"
"io"
_ "embed"
"os"
"os/signal"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"syscall"
"github.com/dop251/goja"
"github.com/ethereum/go-ethereum/console/prompt"
"github.com/ethereum/go-ethereum/internal/jsre"
"github.com/ethereum/go-ethereum/internal/jsre/deps"
"github.com/ethereum/go-ethereum/internal/web3ext"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rpc"
"github.com/mattn/go-colorable"
"github.com/peterh/liner"
"github.com/ethereum/go-ethereum/console/jsrepl"
"github.com/ethereum/go-ethereum/internal/version"
)
var (
// u: unlock, s: signXX, sendXX, n: newAccount, i: importXX
passwordRegexp = regexp.MustCompile(`personal.[nusi]`)
onlyWhitespace = regexp.MustCompile(`^\s*$`)
exit = regexp.MustCompile(`^\s*exit\s*;*\s*$`)
)
// HistoryFile is the file within the data directory to store input scrollback.
const HistoryFile = "history"
// DefaultPrompt is the default prompt line prefix to use for user input querying.
const DefaultPrompt = "> "
// Config is the collection of configurations to fine tune the behavior of the
// JavaScript console.
type Config struct {
DataDir string // Data directory to store the console history at
DocRoot string // Filesystem path from where to load JavaScript files from
Client *rpc.Client // RPC client to execute Ethereum requests through
Prompt string // Input prompt prefix string (defaults to DefaultPrompt)
Prompter prompt.UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout)
Preload []string // Absolute paths to JavaScript files to preload
}
// Console is a JavaScript interpreted runtime environment. It is a fully fledged
// JavaScript console attached to a running node via an external or in-process RPC
// client.
type Console struct {
client *rpc.Client // RPC client to execute Ethereum requests through
jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
prompt string // Input prompt prefix string
prompter prompt.UserPrompter // Input prompter to allow interactive user feedback
histPath string // Absolute path to the console scrollback history
history []string // Scroll history maintained by the console
printer io.Writer // Output writer to serialize any display strings to
interactiveStopped chan struct{}
stopInteractiveCh chan struct{}
signalReceived chan struct{}
stopped chan struct{}
wg sync.WaitGroup
stopOnce sync.Once
}
// New initializes a JavaScript interpreted runtime environment and sets defaults
// with the config struct.
func New(config Config) (*Console, error) {
// Handle unset config values gracefully
if config.Prompter == nil {
config.Prompter = prompt.Stdin
}
if config.Prompt == "" {
config.Prompt = DefaultPrompt
}
if config.Printer == nil {
config.Printer = colorable.NewColorableStdout()
}
// Initialize the console and return
console := &Console{
client: config.Client,
jsre: jsre.New(config.DocRoot, config.Printer),
prompt: config.Prompt,
prompter: config.Prompter,
printer: config.Printer,
histPath: filepath.Join(config.DataDir, HistoryFile),
interactiveStopped: make(chan struct{}),
stopInteractiveCh: make(chan struct{}),
signalReceived: make(chan struct{}, 1),
stopped: make(chan struct{}),
}
if err := os.MkdirAll(config.DataDir, 0700); err != nil {
return nil, err
}
if err := console.init(config.Preload); err != nil {
return nil, err
}
console.wg.Add(1)
go console.interruptHandler()
return console, nil
}
// init retrieves the available APIs from the remote RPC provider and initializes
// the console's JavaScript namespaces based on the exposed modules.
func (c *Console) init(preload []string) error {
c.initConsoleObject()
// Initialize the JavaScript <-> Go RPC bridge.
bridge := newBridge(c.client, c.prompter, c.printer)
if err := c.initWeb3(bridge); err != nil {
return err
}
if err := c.initExtensions(); err != nil {
return err
}
// Add bridge overrides for web3.js functionality.
c.jsre.Do(func(vm *goja.Runtime) {
c.initAdmin(vm, bridge)
c.initPersonal(vm, bridge)
})
// Preload JavaScript files.
for _, path := range preload {
if err := c.jsre.Exec(path); err != nil {
failure := err.Error()
if gojaErr, ok := err.(*goja.Exception); ok {
failure = gojaErr.String()
}
return fmt.Errorf("%s: %v", path, failure)
}
}
// Configure the input prompter for history and tab completion.
if c.prompter != nil {
if content, err := os.ReadFile(c.histPath); err != nil {
c.prompter.SetHistory(nil)
} else {
c.history = strings.Split(string(content), "\n")
c.prompter.SetHistory(c.history)
}
c.prompter.SetWordCompleter(c.AutoCompleteInput)
}
return nil
}
func (c *Console) initConsoleObject() {
c.jsre.Do(func(vm *goja.Runtime) {
console := vm.NewObject()
console.Set("log", c.consoleOutput)
console.Set("error", c.consoleOutput)
vm.Set("console", console)
})
}
func (c *Console) initWeb3(bridge *bridge) error {
if err := c.jsre.Compile("bignumber.js", deps.BigNumberJS); err != nil {
return fmt.Errorf("bignumber.js: %v", err)
}
if err := c.jsre.Compile("web3.js", deps.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
}
var defaultAPIs = map[string]string{"eth": "1.0", "net": "1.0", "debug": "1.0"}
// initExtensions loads and registers web3.js extensions.
func (c *Console) initExtensions() error {
const methodNotFound = -32601
apis, err := c.client.SupportedModules()
// RunAsProc starts up the console and completely switches over the process to
// it, exiting when the console goes down.
func RunAsProc(url string) error {
// Configure the console, cleaning up after
nodejs, bundle, envs, err := configure()
if err != nil {
if rpcErr, ok := err.(rpc.Error); ok && rpcErr.ErrorCode() == methodNotFound {
log.Warn("Server does not support method rpc_modules, using default API list.")
apis = defaultAPIs
} else {
return err
}
}
// Compute aliases from server-provided modules.
aliases := map[string]struct{}{"eth": {}}
for api := range apis {
if api == "web3" {
continue
}
aliases[api] = struct{}{}
if file, ok := web3ext.Modules[api]; ok {
if err = c.jsre.Compile(api+".js", file); err != nil {
return fmt.Errorf("%s.js: %v", api, err)
}
}
}
// Apply aliases.
c.jsre.Do(func(vm *goja.Runtime) {
web3 := getObject(vm, "web3")
for name := range aliases {
if v := web3.Get(name); v != nil {
vm.Set(name, v)
}
}
})
return nil
}
// 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
}
log.Warn("Enabling deprecated personal namespace")
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() {
c.history = nil
c.prompter.ClearHistory()
if err := os.Remove(c.histPath); err != nil {
fmt.Fprintln(c.printer, "can't delete history file:", err)
} else {
fmt.Fprintln(c.printer, "history file deleted")
}
}
// consoleOutput is an override for the console.log and console.error methods to
// stream the output into the configured output stream instead of stdout.
func (c *Console) consoleOutput(call goja.FunctionCall) goja.Value {
var output []string
for _, argument := range call.Arguments {
output = append(output, fmt.Sprintf("%v", argument))
}
fmt.Fprintln(c.printer, strings.Join(output, " "))
return goja.Null()
}
// AutoCompleteInput is a pre-assembled word completer to be used by the user
// input prompter to provide hints to the user about the methods available.
func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) {
// No completions can be provided for empty inputs
if len(line) == 0 || pos == 0 {
return "", nil, ""
}
// Chunk data to relevant part for autocompletion
// E.g. in case of nested lines eth.getBalance(eth.coinb<tab><tab>
start := pos - 1
for ; start > 0; start-- {
// Skip all methods and namespaces (i.e. including the dot)
c := line[start]
if c == '.' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '1' && c <= '9') {
continue
}
// We've hit an unexpected character, autocomplete form here
start++
break
}
return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
}
// Welcome show summary of current Geth instance and some metadata about the
// console's available modules.
func (c *Console) Welcome() {
message := "Welcome to the Geth JavaScript console!\n\n"
// Print some generic Geth metadata
if res, err := c.jsre.Run(`
var message = "instance: " + web3.version.node + "\n";
message += "at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")\n";
try {
message += " datadir: " + admin.datadir + "\n";
} catch (err) {}
message
`); err == nil {
message += res.String()
}
// List all the supported modules for the user to call
if apis, err := c.client.SupportedModules(); err == nil {
modules := make([]string, 0, len(apis))
for api, version := range apis {
modules = append(modules, fmt.Sprintf("%s:%s", api, version))
}
sort.Strings(modules)
message += " modules: " + strings.Join(modules, " ") + "\n"
}
message += "\nTo exit, press ctrl-d or type exit"
fmt.Fprintln(c.printer, message)
}
// Evaluate executes code and pretty prints the result to the specified output
// stream.
func (c *Console) Evaluate(statement string) {
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(c.printer, "[native] error: %v\n", r)
}
}()
c.jsre.Evaluate(statement, c.printer)
// Avoid exiting Interactive when jsre was interrupted by SIGINT.
c.clearSignalReceived()
}
// interruptHandler runs in its own goroutine and waits for signals.
// When a signal is received, it interrupts the JS interpreter.
func (c *Console) interruptHandler() {
defer c.wg.Done()
// During Interactive, liner inhibits the signal while it is prompting for
// input. However, the signal will be received while evaluating JS.
//
// On unsupported terminals, SIGINT can also happen while prompting.
// Unfortunately, it is not possible to abort the prompt in this case and
// the c.readLines goroutine leaks.
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT)
defer signal.Stop(sig)
for {
select {
case <-sig:
c.setSignalReceived()
c.jsre.Interrupt(errors.New("interrupted"))
case <-c.stopInteractiveCh:
close(c.interactiveStopped)
c.jsre.Interrupt(errors.New("interrupted"))
case <-c.stopped:
return
}
}
}
func (c *Console) setSignalReceived() {
select {
case c.signalReceived <- struct{}{}:
default:
}
}
func (c *Console) clearSignalReceived() {
select {
case <-c.signalReceived:
default:
}
}
// StopInteractive causes Interactive to return as soon as possible.
func (c *Console) StopInteractive() {
select {
case c.stopInteractiveCh <- struct{}{}:
case <-c.stopped:
}
}
// Interactive starts an interactive user session, where input is prompted from
// the configured user prompter.
func (c *Console) Interactive() {
var (
prompt = c.prompt // the current prompt line (used for multi-line inputs)
indents = 0 // the current number of input indents (used for multi-line inputs)
input = "" // the current user input
inputLine = make(chan string, 1) // receives user input
inputErr = make(chan error, 1) // receives liner errors
requestLine = make(chan string) // requests a line of input
)
defer func() {
c.writeHistory()
}()
// The line reader runs in a separate goroutine.
go c.readLines(inputLine, inputErr, requestLine)
defer close(requestLine)
for {
// Send the next prompt, triggering an input read.
requestLine <- prompt
select {
case <-c.interactiveStopped:
fmt.Fprintln(c.printer, "node is down, exiting console")
return
case <-c.signalReceived:
// SIGINT received while prompting for input -> unsupported terminal.
// I'm not sure if the best choice would be to leave the console running here.
// Bash keeps running in this case. node.js does not.
fmt.Fprintln(c.printer, "caught interrupt, exiting")
return
case err := <-inputErr:
if err == liner.ErrPromptAborted {
// When prompting for multi-line input, the first Ctrl-C resets
// the multi-line state.
prompt, indents, input = c.prompt, 0, ""
continue
}
return
case line := <-inputLine:
// User input was returned by the prompter, handle special cases.
if indents <= 0 && exit.MatchString(line) {
return
}
if onlyWhitespace.MatchString(line) {
continue
}
// Append the line to the input and check for multi-line interpretation.
input += line + "\n"
indents = countIndents(input)
if indents <= 0 {
prompt = c.prompt
} else {
prompt = strings.Repeat(".", indents*3) + " "
}
// If all the needed lines are present, save the command and run it.
if indents <= 0 {
if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
c.history = append(c.history, command)
if c.prompter != nil {
c.prompter.AppendHistory(command)
}
}
}
c.Evaluate(input)
input = ""
}
}
}
}
// readLines runs in its own goroutine, prompting for input.
func (c *Console) readLines(input chan<- string, errc chan<- error, prompt <-chan string) {
for p := range prompt {
line, err := c.prompter.PromptInput(p)
if err != nil {
errc <- err
} else {
input <- line
}
}
}
// countIndents returns the number of indentations for the given input.
// In case of invalid input such as var a = } the result can be negative.
func countIndents(input string) int {
var (
indents = 0
inString = false
strOpenChar = ' ' // keep track of the string open char to allow var str = "I'm ....";
charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def";
)
for _, c := range input {
switch c {
case '\\':
// indicate next char as escaped when in string and previous char isn't escaping this backslash
if !charEscaped && inString {
charEscaped = true
}
case '\'', '"':
if inString && !charEscaped && strOpenChar == c { // end string
inString = false
} else if !inString && !charEscaped { // begin string
inString = true
strOpenChar = c
}
charEscaped = false
case '{', '(':
if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting
indents++
}
charEscaped = false
case '}', ')':
if !inString {
indents--
}
charEscaped = false
default:
charEscaped = false
}
}
return indents
}
// Stop cleans up the console and terminates the runtime environment.
func (c *Console) Stop(graceful bool) error {
c.stopOnce.Do(func() {
// Stop the interrupt handler.
close(c.stopped)
c.wg.Wait()
})
c.jsre.Stop(graceful)
return nil
}
func (c *Console) writeHistory() error {
if err := os.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil {
return err
}
return os.Chmod(c.histPath, 0600) // Force 0600, even if it was different previously
defer os.Remove(bundle)
// Execute the console, taking over the process space
return syscall.Exec(nodejs, []string{nodejs, bundle, url}, append(envs, syscall.Environ()...))
}
// RunInProc starts up the console inside the current process, multiplexing the
// standard outputs and consuming the standard input.
func RunInProc(url string) error {
// Configure the console, cleaning up after
nodejs, bundle, envs, err := configure()
if err != nil {
return err
}
// Execute the console, keeping it in the current process space
repl := exec.Command(nodejs, bundle, url)
repl.Env = append(envs, os.Environ()...)
repl.Stdin = os.Stdin
repl.Stdout = os.Stdout
repl.Stderr = os.Stderr
return repl.Run()
}
// configure attempts to configure a nodejs based console with a JavaScript REPL
// bundle exported to disk, with some env vars specified.
func configure() (node string, bundle string, envs []string, err error) {
// Find the NodeJS executable
path, err := exec.LookPath("node")
if err != nil {
return "", "", nil, err
}
path, _ = filepath.Abs(path)
// Assemble the js repl bundle script
repl, err := os.CreateTemp("", "tinygeth-console-")
if err != nil {
return "", "", nil, err
}
if _, err = repl.WriteString(jsrepl.Bundle); err != nil {
return "", "", nil, err
}
if err = repl.Close(); err != nil {
return "", "", nil, err
}
// Inject our specific APIs and ENV variables
envs = []string{
"TINYGETH_CONSOLE_VERSION=" + version.WithMeta,
}
// Return everything for different startups
return path, repl.Name(), envs, nil
}

View file

@ -1,321 +0,0 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package console
import (
"bytes"
"errors"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/console/prompt"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/internal/jsre"
"github.com/ethereum/go-ethereum/miner"
"github.com/ethereum/go-ethereum/node"
)
const (
testInstance = "console-tester"
testAddress = "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182"
)
// hookedPrompter implements UserPrompter to simulate use input via channels.
type hookedPrompter struct {
scheduler chan string
}
func (p *hookedPrompter) PromptInput(prompt string) (string, error) {
// Send the prompt to the tester
select {
case p.scheduler <- prompt:
case <-time.After(time.Second):
return "", errors.New("prompt timeout")
}
// Retrieve the response and feed to the console
select {
case input := <-p.scheduler:
return input, nil
case <-time.After(time.Second):
return "", errors.New("input timeout")
}
}
func (p *hookedPrompter) PromptPassword(prompt string) (string, error) {
return "", errors.New("not implemented")
}
func (p *hookedPrompter) PromptConfirm(prompt string) (bool, error) {
return false, errors.New("not implemented")
}
func (p *hookedPrompter) SetHistory(history []string) {}
func (p *hookedPrompter) AppendHistory(command string) {}
func (p *hookedPrompter) ClearHistory() {}
func (p *hookedPrompter) SetWordCompleter(completer prompt.WordCompleter) {}
// tester is a console test environment for the console tests to operate on.
type tester struct {
workspace string
stack *node.Node
ethereum *eth.Ethereum
console *Console
input *hookedPrompter
output *bytes.Buffer
}
// newTester creates a test environment based on which the console can operate.
// Please ensure you call Close() on the returned tester to avoid leaks.
func newTester(t *testing.T, confOverride func(*ethconfig.Config)) *tester {
// Create a temporary storage for the node keys and initialize it
workspace := t.TempDir()
// Create a networkless protocol stack and start an Ethereum service within
stack, err := node.New(&node.Config{DataDir: workspace, UseLightweightKDF: true, Name: testInstance})
if err != nil {
t.Fatalf("failed to create node: %v", err)
}
ethConf := &ethconfig.Config{
Genesis: core.DeveloperGenesisBlock(11_500_000, nil),
Miner: miner.Config{
PendingFeeRecipient: common.HexToAddress(testAddress),
},
}
if confOverride != nil {
confOverride(ethConf)
}
ethBackend, err := eth.New(stack, ethConf)
if err != nil {
t.Fatalf("failed to register Ethereum protocol: %v", err)
}
// Start the node and assemble the JavaScript console around it
if err = stack.Start(); err != nil {
t.Fatalf("failed to start test stack: %v", err)
}
client := stack.Attach()
t.Cleanup(func() {
client.Close()
})
prompter := &hookedPrompter{scheduler: make(chan string)}
printer := new(bytes.Buffer)
console, err := New(Config{
DataDir: stack.DataDir(),
DocRoot: "testdata",
Client: client,
Prompter: prompter,
Printer: printer,
Preload: []string{"preload.js"},
})
if err != nil {
t.Fatalf("failed to create JavaScript console: %v", err)
}
// Create the final tester and return
return &tester{
workspace: workspace,
stack: stack,
ethereum: ethBackend,
console: console,
input: prompter,
output: printer,
}
}
// Close cleans up any temporary data folders and held resources.
func (env *tester) Close(t *testing.T) {
if err := env.console.Stop(false); err != nil {
t.Errorf("failed to stop embedded console: %v", err)
}
if err := env.stack.Close(); err != nil {
t.Errorf("failed to tear down embedded node: %v", err)
}
os.RemoveAll(env.workspace)
}
// Tests that the node lists the correct welcome message, notably that it contains
// the instance name, block number, data directory and supported console modules.
func TestWelcome(t *testing.T) {
tester := newTester(t, nil)
defer tester.Close(t)
tester.console.Welcome()
output := tester.output.String()
if want := "Welcome"; !strings.Contains(output, want) {
t.Fatalf("console output missing welcome message: have\n%s\nwant also %s", output, want)
}
if want := fmt.Sprintf("instance: %s", testInstance); !strings.Contains(output, want) {
t.Fatalf("console output missing instance: have\n%s\nwant also %s", output, want)
}
if want := "at block: 0"; !strings.Contains(output, want) {
t.Fatalf("console output missing sync status: have\n%s\nwant also %s", output, want)
}
if want := fmt.Sprintf("datadir: %s", tester.workspace); !strings.Contains(output, want) {
t.Fatalf("console output missing datadir: have\n%s\nwant also %s", output, want)
}
if want := "modules: "; !strings.Contains(output, want) {
t.Fatalf("console output missing modules: have\n%s\nwant also %s", output, want)
}
}
// Tests that JavaScript statement evaluation works as intended.
func TestEvaluate(t *testing.T) {
tester := newTester(t, nil)
defer tester.Close(t)
tester.console.Evaluate("2 + 2")
if output := tester.output.String(); !strings.Contains(output, "4") {
t.Fatalf("statement evaluation failed: have %s, want %s", output, "4")
}
}
// Tests that the console can be used in interactive mode.
func TestInteractive(t *testing.T) {
// Create a tester and run an interactive console in the background
tester := newTester(t, nil)
defer tester.Close(t)
go tester.console.Interactive()
// Wait for a prompt and send a statement back
select {
case <-tester.input.scheduler:
case <-time.After(time.Second):
t.Fatalf("initial prompt timeout")
}
select {
case tester.input.scheduler <- "2+2":
case <-time.After(time.Second):
t.Fatalf("input feedback timeout")
}
// Wait for the second prompt and ensure first statement was evaluated
select {
case <-tester.input.scheduler:
case <-time.After(time.Second):
t.Fatalf("secondary prompt timeout")
}
if output := tester.output.String(); !strings.Contains(output, "4") {
t.Fatalf("statement evaluation failed: have %s, want %s", output, "4")
}
}
// Tests that preloaded JavaScript files have been executed before user is given
// input.
func TestPreload(t *testing.T) {
tester := newTester(t, nil)
defer tester.Close(t)
tester.console.Evaluate("preloaded")
if output := tester.output.String(); !strings.Contains(output, "some-preloaded-string") {
t.Fatalf("preloaded variable missing: have %s, want %s", output, "some-preloaded-string")
}
}
// Tests that the JavaScript objects returned by statement executions are properly
// pretty printed instead of just displaying "[object]".
func TestPrettyPrint(t *testing.T) {
tester := newTester(t, nil)
defer tester.Close(t)
tester.console.Evaluate("obj = {int: 1, string: 'two', list: [3, 3, 3], obj: {null: null, func: function(){}}}")
// Define some specially formatted fields
var (
one = jsre.NumberColor("1")
two = jsre.StringColor("\"two\"")
three = jsre.NumberColor("3")
null = jsre.SpecialColor("null")
fun = jsre.FunctionColor("function()")
)
// Assemble the actual output we're after and verify
want := `{
int: ` + one + `,
list: [` + three + `, ` + three + `, ` + three + `],
obj: {
null: ` + null + `,
func: ` + fun + `
},
string: ` + two + `
}
`
if output := tester.output.String(); output != want {
t.Fatalf("pretty print mismatch: have %s, want %s", output, want)
}
}
// Tests that the JavaScript exceptions are properly formatted and colored.
func TestPrettyError(t *testing.T) {
tester := newTester(t, nil)
defer tester.Close(t)
tester.console.Evaluate("throw 'hello'")
want := jsre.ErrorColor("hello") + "\n\tat <eval>:1:1(1)\n\n"
if output := tester.output.String(); output != want {
t.Fatalf("pretty error mismatch: have %s, want %s", output, want)
}
}
// Tests that tests if the number of indents for JS input is calculated correct.
func TestIndenting(t *testing.T) {
testCases := []struct {
input string
expectedIndentCount int
}{
{`var a = 1;`, 0},
{`"some string"`, 0},
{`"some string with (parenthesis`, 0},
{`"some string with newline
("`, 0},
{`function v(a,b) {}`, 0},
{`function f(a,b) { var str = "asd("; };`, 0},
{`function f(a) {`, 1},
{`function f(a, function(b) {`, 2},
{`function f(a, function(b) {
var str = "a)}";
});`, 0},
{`function f(a,b) {
var str = "a{b(" + a, ", " + b;
}`, 0},
{`var str = "\"{"`, 0},
{`var str = "'("`, 0},
{`var str = "\\{"`, 0},
{`var str = "\\\\{"`, 0},
{`var str = 'a"{`, 0},
{`var obj = {`, 1},
{`var obj = { {a:1`, 2},
{`var obj = { {a:1}`, 1},
{`var obj = { {a:1}, b:2}`, 0},
{`var obj = {}`, 0},
{`var obj = {
a: 1, b: 2
}`, 0},
{`var test = }`, -1},
{`var str = "a\""; var obj = {`, 1},
}
for i, tt := range testCases {
counted := countIndents(tt.input)
if counted != tt.expectedIndentCount {
t.Errorf("test %d: invalid indenting: have %d, want %d", i, counted, tt.expectedIndentCount)
}
}
}

69
console/jsrepl/bundle.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
// Copyright 2020 The go-ethereum Authors
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
@ -14,35 +14,17 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package console
package jsrepl
import (
"testing"
"github.com/dop251/goja"
"github.com/ethereum/go-ethereum/internal/jsre"
_ "embed"
)
// TestUndefinedAsParam ensures that personal functions can receive
// `undefined` as a parameter.
func TestUndefinedAsParam(t *testing.T) {
b := bridge{}
call := jsre.Call{}
call.Arguments = []goja.Value{goja.Undefined()}
//go:generate yarn install
//go:generate npx esbuild jsrepl.js --bundle --platform=node --minify --keep-names --outfile=bundle.js
b.UnlockAccount(call)
b.Sign(call)
b.Sleep(call)
}
// TestNullAsParam ensures that personal functions can receive
// `null` as a parameter.
func TestNullAsParam(t *testing.T) {
b := bridge{}
call := jsre.Call{}
call.Arguments = []goja.Value{goja.Null()}
b.UnlockAccount(call)
b.Sign(call)
b.Sleep(call)
}
// Bundle is the compiled together NodeJS module containing Ethers.js, a pretty
// printed JavaScript REPL interpreter and some Geth bridge code.
//
//go:embed bundle.js
var Bundle string

114
console/jsrepl/jsrepl.js Normal file
View file

@ -0,0 +1,114 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// Pull in a few packages needed for the console
const ethers = require('ethers');
const repl = require('pretty-repl');
const util = require('util');
const fs = require('fs');
import chalk from 'chalk';
// Connect to the requested node and create an API wrapper
function dial(url) {
if (url.startsWith('ws://') || url.startsWith('wss://')) {
return new ethers.WebSocketProvider(url);
} else if (url.startsWith('http://') || url.startsWith('https://')) {
return new ethers.JsonRpcProvider(url);
} else {
return new ethers.IpcSocketProvider(url);
}
}
const pubapi = dial(process.argv[2]);
// Ethers.js is opinionated, swallowing certain fields from responses. Whilst
// that is fine for the user API, for the Geth console APIs, we would like to
// have everything exposed for cleaner testing. Create a secondary API object
// that injects all missing fields back (sorry ethers).
const dbgapi = dial(process.argv[2]);
const oldWrapBlock = dbgapi._wrapBlock.bind(dbgapi);
dbgapi._wrapBlock = (value, format) => {
const block = oldWrapBlock(value, format);
Object.keys(value).forEach((key) => {
if (!(key in block)) {
block[key] = value[key]; // Transform anything here if need be
}
});
return block;
};
// Collect all the custom methods we want to expose
const context = {
ethers: ethers, // Expose ethers for power users
client: pubapi, // Expose the original client provider
hooked: dbgapi, // Expose the hooked client provider
// Define the Geth specific specialized methods (TODO)
eth: {
getBlock: dbgapi.getBlock.bind(dbgapi),
}
};
// Print some startup headers
const welcome = async () => {
const client = await pubapi.send("web3_clientVersion");
const block = await pubapi.getBlock();
const modules = await pubapi.send("rpc_modules");
console.log(`Welcome to the Geth console!\n`);
console.log(`Geth: ${chalk.green("go-ethereum")} ${chalk.yellow("v" + process.env.TINYGETH_CONSOLE_VERSION)}`);
console.log(`REPL: ${chalk.green(" nodejs")} ${chalk.yellow(process.version)}`);
console.log(`Web3: ${chalk.green(" ethers")} ${chalk.yellow("v" + ethers.version)}\n`);
console.log(`Attached: ${client}`);
console.log(`At block: ${block.number} (${new Date(1000 * Number(block.timestamp))})`);
console.log(` Exposed: ${Object.keys(modules).join(' ')}\n`);
console.log(chalk.grey("• The web3 library uses promises, you need to await appropriately."));
console.log(chalk.grey("• The usual Geth API methods are exposed to the root namespace."));
console.log(chalk.grey("• The web3 library is exposed in full in the `ethers` field."));
console.log(chalk.grey("• The web3 connection is exposed via the `client` field."));
console.log(chalk.grey("• The `hooked` client exposes all data from the RPC.\n"));
};
// Start the REPL server and inject all context into it
const startup = async () => {
const server = repl.start({
ignoreUndefined: true,
useGlobal: true,
prompt: chalk.green('→ '),
writer: (output) => {
if (output && typeof output.stack === 'string' && typeof output.message === 'string') {
return chalk.red(output.stack || output.message);
}
return chalk.gray(util.inspect(output, {colors: true, depth: null}));
}
});
server.on('exit', () => {
process.exit(); // Force tear down all resources
});
Object.assign(server.context, context);
};
welcome().then(() => startup());
// REPL started and entire script evaluated, self-destruct. This is a weird one
// but since we can't delete from Go (process reown), this is the only remaining
// place to clean up the script.
fs.unlinkSync(process.argv[1]);

View file

@ -0,0 +1,7 @@
{
"dependencies": {
"chalk": "^5.3.0",
"ethers": "^6.13.4",
"pretty-repl": "^4.0.1"
}
}

160
console/jsrepl/yarn.lock Normal file
View file

@ -0,0 +1,160 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@adraffy/ens-normalize@1.10.1":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069"
integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==
"@noble/curves@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35"
integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==
dependencies:
"@noble/hashes" "1.3.2"
"@noble/hashes@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
"@types/node@22.7.5":
version "22.7.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b"
integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==
dependencies:
undici-types "~6.19.2"
aes-js@4.0.0-beta.5:
version "4.0.0-beta.5"
resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873"
integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
chalk@^4.0.0, chalk@^4.1.1:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
emphasize@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/emphasize/-/emphasize-4.2.0.tgz#6b6fdc4d212cb7eafea1c7cdd595dfd6cfc508d9"
integrity sha512-yGKvcFUHlBsUPwlxTlzKLR8+zhpbitkFOMCUxN8fTJng9bdH3WNzUGkhdaGdjndSUgqmMPBN7umfwnUdLz5Axg==
dependencies:
chalk "^4.0.0"
highlight.js "~10.4.0"
lowlight "~1.17.0"
ethers@^6.13.4:
version "6.13.4"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.13.4.tgz#bd3e1c3dc1e7dc8ce10f9ffb4ee40967a651b53c"
integrity sha512-21YtnZVg4/zKkCQPjrDj38B1r4nQvTZLopUGMLQ1ePU2zV/joCfDC3t3iKQjWRzjjjbzR+mdAIoikeBRNkdllA==
dependencies:
"@adraffy/ens-normalize" "1.10.1"
"@noble/curves" "1.2.0"
"@noble/hashes" "1.3.2"
"@types/node" "22.7.5"
aes-js "4.0.0-beta.5"
tslib "2.7.0"
ws "8.17.1"
fault@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==
dependencies:
format "^0.2.0"
format@^0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
highlight.js@~10.4.0:
version "10.4.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0"
integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==
lowlight@~1.17.0:
version "1.17.0"
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.17.0.tgz#a1143b2fba8239df8cd5893f9fe97aaf8465af4a"
integrity sha512-vmtBgYKD+QVNy7tIa7ulz5d//Il9R4MooOVh4nkOf9R9Cb/Dk5TXMSTieg/vDulkBkIWj59/BIlyFQxT9X1oAQ==
dependencies:
fault "^1.0.0"
highlight.js "~10.4.0"
pretty-repl@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pretty-repl/-/pretty-repl-4.0.1.tgz#b449d775c82a13cb71474a880f682fc5ef327dde"
integrity sha512-Ve+ZNS5fwxylks3TTR4su7SaNAHVOh++7J5R8VKFAHIjmAMS8X79rnETc/JJoqay52cfgeHum7vm2+9hFSys9Q==
dependencies:
ansi-regex "^5.0.1"
chalk "^4.1.1"
emphasize "^4.2.0"
strip-ansi "^6.0.0"
strip-ansi@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"
tslib@2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
undici-types@~6.19.2:
version "6.19.8"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
ws@8.17.1:
version "8.17.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==

View file

@ -1 +0,0 @@
var preloaded = "some-preloaded-string";

View file

@ -1,93 +0,0 @@
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package jsre
import (
"regexp"
"sort"
"strings"
"github.com/dop251/goja"
)
// JS numerical token
var numerical = regexp.MustCompile(`^(NaN|-?((\d*\.\d+|\d+)([Ee][+-]?\d+)?|Infinity))$`)
// CompleteKeywords returns potential continuations for the given line. Since line is
// evaluated, callers need to make sure that evaluating line does not have side effects.
func (jsre *JSRE) CompleteKeywords(line string) []string {
var results []string
jsre.Do(func(vm *goja.Runtime) {
results = getCompletions(vm, line)
})
return results
}
func getCompletions(vm *goja.Runtime, line string) (results []string) {
parts := strings.Split(line, ".")
if len(parts) == 0 {
return nil
}
// Find the right-most fully named object in the line. e.g. if line = "x.y.z"
// and "x.y" is an object, obj will reference "x.y".
obj := vm.GlobalObject()
for i := 0; i < len(parts)-1; i++ {
if numerical.MatchString(parts[i]) {
return nil
}
v := obj.Get(parts[i])
if v == nil || goja.IsNull(v) || goja.IsUndefined(v) {
return nil // No object was found
}
obj = v.ToObject(vm)
}
// Go over the keys of the object and retain the keys matching prefix.
// Example: if line = "x.y.z" and "x.y" exists and has keys "zebu", "zebra"
// and "platypus", then "x.y.zebu" and "x.y.zebra" will be added to results.
prefix := parts[len(parts)-1]
iterOwnAndConstructorKeys(vm, obj, func(k string) {
if strings.HasPrefix(k, prefix) {
if len(parts) == 1 {
results = append(results, k)
} else {
results = append(results, strings.Join(parts[:len(parts)-1], ".")+"."+k)
}
}
})
// Append opening parenthesis (for functions) or dot (for objects)
// if the line itself is the only completion.
if len(results) == 1 && results[0] == line {
// Accessing the property will cause it to be evaluated.
// This can cause an error, e.g. in case of web3.eth.protocolVersion
// which has been dropped from geth. Ignore the error for autocompletion
// purposes.
obj := SafeGet(obj, parts[len(parts)-1])
if obj != nil {
if _, isfunc := goja.AssertFunction(obj); isfunc {
results[0] += "("
} else {
results[0] += "."
}
}
}
sort.Strings(results)
return results
}

View file

@ -1,99 +0,0 @@
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package jsre
import (
"os"
"reflect"
"testing"
)
func TestCompleteKeywords(t *testing.T) {
t.Parallel()
re := New("", os.Stdout)
re.Run(`
function theClass() {
this.foo = 3;
this.gazonk = {xyz: 4};
}
theClass.prototype.someMethod = function () {};
var x = new theClass();
var y = new theClass();
y.someMethod = function override() {};
`)
var tests = []struct {
input string
want []string
}{
{
input: "St",
want: []string{"String"},
},
{
input: "x",
want: []string{"x."},
},
{
input: "x.someMethod",
want: []string{"x.someMethod("},
},
{
input: "x.",
want: []string{
"x.constructor",
"x.foo",
"x.gazonk",
"x.someMethod",
},
},
{
input: "y.",
want: []string{
"y.constructor",
"y.foo",
"y.gazonk",
"y.someMethod",
},
},
{
input: "x.gazonk.",
want: []string{
"x.gazonk.__proto__",
"x.gazonk.constructor",
"x.gazonk.hasOwnProperty",
"x.gazonk.isPrototypeOf",
"x.gazonk.propertyIsEnumerable",
"x.gazonk.toLocaleString",
"x.gazonk.toString",
"x.gazonk.valueOf",
"x.gazonk.xyz",
},
},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
t.Parallel()
cs := re.CompleteKeywords(test.input)
if !reflect.DeepEqual(cs, test.want) {
t.Errorf("wrong completions for %q\ngot %v\nwant %v", test.input, cs, test.want)
}
})
}
}

View file

@ -1,340 +0,0 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// Package jsre provides execution environment for JavaScript.
package jsre
import (
crand "crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"math/rand"
"os"
"time"
"github.com/dop251/goja"
"github.com/ethereum/go-ethereum/common"
)
// JSRE is a JS runtime environment embedding the goja interpreter.
// It provides helper functions to load code from files, run code snippets
// and bind native go objects to JS.
//
// The runtime runs all code on a dedicated event loop and does not expose the underlying
// goja runtime directly. To use the runtime, call JSRE.Do. When binding a Go function,
// use the Call type to gain access to the runtime.
type JSRE struct {
assetPath string
output io.Writer
evalQueue chan *evalReq
stopEventLoop chan bool
closed chan struct{}
vm *goja.Runtime
}
// Call is the argument type of Go functions which are callable from JS.
type Call struct {
goja.FunctionCall
VM *goja.Runtime
}
// jsTimer is a single timer instance with a callback function
type jsTimer struct {
timer *time.Timer
duration time.Duration
interval bool
call goja.FunctionCall
}
// evalReq is a serialized vm execution request processed by runEventLoop.
type evalReq struct {
fn func(vm *goja.Runtime)
done chan bool
}
// runtime must be stopped with Stop() after use and cannot be used after stopping
func New(assetPath string, output io.Writer) *JSRE {
re := &JSRE{
assetPath: assetPath,
output: output,
closed: make(chan struct{}),
evalQueue: make(chan *evalReq),
stopEventLoop: make(chan bool),
vm: goja.New(),
}
go re.runEventLoop()
re.Set("loadScript", MakeCallback(re.vm, re.loadScript))
re.Set("inspect", re.prettyPrintJS)
return re
}
// randomSource returns a pseudo random value generator.
func randomSource() *rand.Rand {
bytes := make([]byte, 8)
seed := time.Now().UnixNano()
if _, err := crand.Read(bytes); err == nil {
seed = int64(binary.LittleEndian.Uint64(bytes))
}
src := rand.NewSource(seed)
return rand.New(src)
}
// This function runs the main event loop from a goroutine that is started
// when JSRE is created. Use Stop() before exiting to properly stop it.
// The event loop processes vm access requests from the evalQueue in a
// serialized way and calls timer callback functions at the appropriate time.
// Exported functions always access the vm through the event queue. You can
// call the functions of the goja vm directly to circumvent the queue. These
// functions should be used if and only if running a routine that was already
// called from JS through an RPC call.
func (re *JSRE) runEventLoop() {
defer close(re.closed)
r := randomSource()
re.vm.SetRandSource(r.Float64)
registry := map[*jsTimer]*jsTimer{}
ready := make(chan *jsTimer)
newTimer := func(call goja.FunctionCall, interval bool) (*jsTimer, goja.Value) {
delay := call.Argument(1).ToInteger()
if 0 >= delay {
delay = 1
}
timer := &jsTimer{
duration: time.Duration(delay) * time.Millisecond,
call: call,
interval: interval,
}
registry[timer] = timer
timer.timer = time.AfterFunc(timer.duration, func() {
ready <- timer
})
return timer, re.vm.ToValue(timer)
}
setTimeout := func(call goja.FunctionCall) goja.Value {
_, value := newTimer(call, false)
return value
}
setInterval := func(call goja.FunctionCall) goja.Value {
_, value := newTimer(call, true)
return value
}
clearTimeout := func(call goja.FunctionCall) goja.Value {
timer := call.Argument(0).Export()
if timer, ok := timer.(*jsTimer); ok {
timer.timer.Stop()
delete(registry, timer)
}
return goja.Undefined()
}
re.vm.Set("_setTimeout", setTimeout)
re.vm.Set("_setInterval", setInterval)
re.vm.RunString(`var setTimeout = function(args) {
if (arguments.length < 1) {
throw TypeError("Failed to execute 'setTimeout': 1 argument required, but only 0 present.");
}
return _setTimeout.apply(this, arguments);
}`)
re.vm.RunString(`var setInterval = function(args) {
if (arguments.length < 1) {
throw TypeError("Failed to execute 'setInterval': 1 argument required, but only 0 present.");
}
return _setInterval.apply(this, arguments);
}`)
re.vm.Set("clearTimeout", clearTimeout)
re.vm.Set("clearInterval", clearTimeout)
var waitForCallbacks bool
loop:
for {
select {
case timer := <-ready:
// execute callback, remove/reschedule the timer
var arguments []interface{}
if len(timer.call.Arguments) > 2 {
tmp := timer.call.Arguments[2:]
arguments = make([]interface{}, 2+len(tmp))
for i, value := range tmp {
arguments[i+2] = value
}
} else {
arguments = make([]interface{}, 1)
}
arguments[0] = timer.call.Arguments[0]
call, isFunc := goja.AssertFunction(timer.call.Arguments[0])
if !isFunc {
panic(re.vm.ToValue("js error: timer/timeout callback is not a function"))
}
call(goja.Null(), timer.call.Arguments...)
_, inreg := registry[timer] // when clearInterval is called from within the callback don't reset it
if timer.interval && inreg {
timer.timer.Reset(timer.duration)
} else {
delete(registry, timer)
if waitForCallbacks && (len(registry) == 0) {
break loop
}
}
case req := <-re.evalQueue:
// run the code, send the result back
req.fn(re.vm)
close(req.done)
if waitForCallbacks && (len(registry) == 0) {
break loop
}
case waitForCallbacks = <-re.stopEventLoop:
if !waitForCallbacks || (len(registry) == 0) {
break loop
}
}
}
for _, timer := range registry {
timer.timer.Stop()
delete(registry, timer)
}
}
// Do executes the given function on the JS event loop.
// When the runtime is stopped, fn will not execute.
func (re *JSRE) Do(fn func(*goja.Runtime)) {
done := make(chan bool)
req := &evalReq{fn, done}
select {
case re.evalQueue <- req:
<-done
case <-re.closed:
}
}
// Stop terminates the event loop, optionally waiting for all timers to expire.
func (re *JSRE) Stop(waitForCallbacks bool) {
timeout := time.NewTimer(10 * time.Millisecond)
defer timeout.Stop()
for {
select {
case <-re.closed:
return
case re.stopEventLoop <- waitForCallbacks:
<-re.closed
return
case <-timeout.C:
// JS is blocked, interrupt and try again.
re.vm.Interrupt(errors.New("JS runtime stopped"))
}
}
}
// Exec(file) loads and runs the contents of a file
// if a relative path is given, the jsre's assetPath is used
func (re *JSRE) Exec(file string) error {
code, err := os.ReadFile(common.AbsolutePath(re.assetPath, file))
if err != nil {
return err
}
return re.Compile(file, string(code))
}
// Run runs a piece of JS code.
func (re *JSRE) Run(code string) (v goja.Value, err error) {
re.Do(func(vm *goja.Runtime) { v, err = vm.RunString(code) })
return v, err
}
// Set assigns value v to a variable in the JS environment.
func (re *JSRE) Set(ns string, v interface{}) (err error) {
re.Do(func(vm *goja.Runtime) { vm.Set(ns, v) })
return err
}
// MakeCallback turns the given function into a function that's callable by JS.
func MakeCallback(vm *goja.Runtime, fn func(Call) (goja.Value, error)) goja.Value {
return vm.ToValue(func(call goja.FunctionCall) goja.Value {
result, err := fn(Call{call, vm})
if err != nil {
panic(vm.NewGoError(err))
}
return result
})
}
// Evaluate executes code and pretty prints the result to the specified output stream.
func (re *JSRE) Evaluate(code string, w io.Writer) {
re.Do(func(vm *goja.Runtime) {
val, err := vm.RunString(code)
if err != nil {
prettyError(vm, err, w)
} else {
prettyPrint(vm, val, w)
}
fmt.Fprintln(w)
})
}
// Interrupt stops the current JS evaluation.
func (re *JSRE) Interrupt(v interface{}) {
done := make(chan bool)
noop := func(*goja.Runtime) {}
select {
case re.evalQueue <- &evalReq{noop, done}:
// event loop is not blocked.
default:
re.vm.Interrupt(v)
}
}
// Compile compiles and then runs a piece of JS code.
func (re *JSRE) Compile(filename string, src string) (err error) {
re.Do(func(vm *goja.Runtime) { _, err = compileAndRun(vm, filename, src) })
return err
}
// loadScript loads and executes a JS file.
func (re *JSRE) loadScript(call Call) (goja.Value, error) {
file := call.Argument(0).ToString().String()
file = common.AbsolutePath(re.assetPath, file)
source, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("could not read file %s: %v", file, err)
}
value, err := compileAndRun(re.vm, file, string(source))
if err != nil {
return nil, fmt.Errorf("error while compiling or running script: %v", err)
}
return value, nil
}
func compileAndRun(vm *goja.Runtime, filename string, src string) (goja.Value, error) {
script, err := goja.Compile(filename, src, false)
if err != nil {
return goja.Null(), err
}
return vm.RunProgram(script)
}

View file

@ -1,138 +0,0 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package jsre
import (
"os"
"path/filepath"
"reflect"
"testing"
"time"
"github.com/dop251/goja"
)
type testNativeObjectBinding struct {
vm *goja.Runtime
}
type msg struct {
Msg string
}
func (no *testNativeObjectBinding) TestMethod(call goja.FunctionCall) goja.Value {
m := call.Argument(0).ToString().String()
return no.vm.ToValue(&msg{m})
}
func newWithTestJS(t *testing.T, testjs string) *JSRE {
dir := t.TempDir()
if testjs != "" {
if err := os.WriteFile(filepath.Join(dir, "test.js"), []byte(testjs), os.ModePerm); err != nil {
t.Fatal("cannot create test.js:", err)
}
}
jsre := New(dir, os.Stdout)
return jsre
}
func TestExec(t *testing.T) {
t.Parallel()
jsre := newWithTestJS(t, `msg = "testMsg"`)
err := jsre.Exec("test.js")
if err != nil {
t.Errorf("expected no error, got %v", err)
}
val, err := jsre.Run("msg")
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if val.ExportType().Kind() != reflect.String {
t.Errorf("expected string value, got %v", val)
}
exp := "testMsg"
got := val.ToString().String()
if exp != got {
t.Errorf("expected '%v', got '%v'", exp, got)
}
jsre.Stop(false)
}
func TestNatto(t *testing.T) {
t.Parallel()
jsre := newWithTestJS(t, `setTimeout(function(){msg = "testMsg"}, 1);`)
err := jsre.Exec("test.js")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
time.Sleep(100 * time.Millisecond)
val, err := jsre.Run("msg")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if val.ExportType().Kind() != reflect.String {
t.Fatalf("expected string value, got %v", val)
}
exp := "testMsg"
got := val.ToString().String()
if exp != got {
t.Fatalf("expected '%v', got '%v'", exp, got)
}
jsre.Stop(false)
}
func TestBind(t *testing.T) {
t.Parallel()
jsre := New("", os.Stdout)
defer jsre.Stop(false)
jsre.Set("no", &testNativeObjectBinding{vm: jsre.vm})
_, err := jsre.Run(`no.TestMethod("testMsg")`)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
}
func TestLoadScript(t *testing.T) {
t.Parallel()
jsre := newWithTestJS(t, `msg = "testMsg"`)
_, err := jsre.Run(`loadScript("test.js")`)
if err != nil {
t.Errorf("expected no error, got %v", err)
}
val, err := jsre.Run("msg")
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if val.ExportType().Kind() != reflect.String {
t.Errorf("expected string value, got %v", val)
}
exp := "testMsg"
got := val.ToString().String()
if exp != got {
t.Errorf("expected '%v', got '%v'", exp, got)
}
jsre.Stop(false)
}

View file

@ -1,301 +0,0 @@
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package jsre
import (
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
"github.com/dop251/goja"
"github.com/fatih/color"
)
const (
maxPrettyPrintLevel = 3
indentString = " "
)
var (
FunctionColor = color.New(color.FgMagenta).SprintfFunc()
SpecialColor = color.New(color.Bold).SprintfFunc()
NumberColor = color.New(color.FgRed).SprintfFunc()
StringColor = color.New(color.FgGreen).SprintfFunc()
ErrorColor = color.New(color.FgHiRed).SprintfFunc()
)
// these fields are hidden when printing objects.
var boringKeys = map[string]bool{
"valueOf": true,
"toString": true,
"toLocaleString": true,
"hasOwnProperty": true,
"isPrototypeOf": true,
"propertyIsEnumerable": true,
"constructor": true,
}
// prettyPrint writes value to standard output.
func prettyPrint(vm *goja.Runtime, value goja.Value, w io.Writer) {
ppctx{vm: vm, w: w}.printValue(value, 0, false)
}
// prettyError writes err to standard output.
func prettyError(vm *goja.Runtime, err error, w io.Writer) {
failure := err.Error()
if gojaErr, ok := err.(*goja.Exception); ok {
failure = gojaErr.String()
}
fmt.Fprint(w, ErrorColor("%s", failure))
}
func (re *JSRE) prettyPrintJS(call goja.FunctionCall) goja.Value {
for _, v := range call.Arguments {
prettyPrint(re.vm, v, re.output)
fmt.Fprintln(re.output)
}
return goja.Undefined()
}
type ppctx struct {
vm *goja.Runtime
w io.Writer
}
func (ctx ppctx) indent(level int) string {
return strings.Repeat(indentString, level)
}
func (ctx ppctx) printValue(v goja.Value, level int, inArray bool) {
if goja.IsNull(v) || goja.IsUndefined(v) {
fmt.Fprint(ctx.w, SpecialColor(v.String()))
return
}
kind := v.ExportType().Kind()
switch {
case kind == reflect.Bool:
fmt.Fprint(ctx.w, SpecialColor("%t", v.ToBoolean()))
case kind == reflect.String:
fmt.Fprint(ctx.w, StringColor("%q", v.String()))
case kind >= reflect.Int && kind <= reflect.Complex128:
fmt.Fprint(ctx.w, NumberColor("%s", v.String()))
default:
if obj, ok := v.(*goja.Object); ok {
ctx.printObject(obj, level, inArray)
} else {
fmt.Fprintf(ctx.w, "<unprintable %T>", v)
}
}
}
// SafeGet attempt to get the value associated to `key`, and
// catches the panic that goja creates if an error occurs in
// key.
func SafeGet(obj *goja.Object, key string) (ret goja.Value) {
defer func() {
if r := recover(); r != nil {
ret = goja.Undefined()
}
}()
ret = obj.Get(key)
return ret
}
func (ctx ppctx) printObject(obj *goja.Object, level int, inArray bool) {
switch obj.ClassName() {
case "Array", "GoArray":
lv := obj.Get("length")
len := lv.ToInteger()
if len == 0 {
fmt.Fprintf(ctx.w, "[]")
return
}
if level > maxPrettyPrintLevel {
fmt.Fprint(ctx.w, "[...]")
return
}
fmt.Fprint(ctx.w, "[")
for i := int64(0); i < len; i++ {
el := obj.Get(strconv.FormatInt(i, 10))
if el != nil {
ctx.printValue(el, level+1, true)
}
if i < len-1 {
fmt.Fprintf(ctx.w, ", ")
}
}
fmt.Fprint(ctx.w, "]")
case "Object":
// Print values from bignumber.js as regular numbers.
if ctx.isBigNumber(obj) {
fmt.Fprint(ctx.w, NumberColor("%s", toString(obj)))
return
}
// Otherwise, print all fields indented, but stop if we're too deep.
keys := ctx.fields(obj)
if len(keys) == 0 {
fmt.Fprint(ctx.w, "{}")
return
}
if level > maxPrettyPrintLevel {
fmt.Fprint(ctx.w, "{...}")
return
}
fmt.Fprintln(ctx.w, "{")
for i, k := range keys {
v := SafeGet(obj, k)
fmt.Fprintf(ctx.w, "%s%s: ", ctx.indent(level+1), k)
ctx.printValue(v, level+1, false)
if i < len(keys)-1 {
fmt.Fprintf(ctx.w, ",")
}
fmt.Fprintln(ctx.w)
}
if inArray {
level--
}
fmt.Fprintf(ctx.w, "%s}", ctx.indent(level))
case "Function":
robj := obj.ToString()
desc := strings.Trim(strings.Split(robj.String(), "{")[0], " \t\n")
desc = strings.Replace(desc, " (", "(", 1)
fmt.Fprint(ctx.w, FunctionColor("%s", desc))
case "RegExp":
fmt.Fprint(ctx.w, StringColor("%s", toString(obj)))
default:
if level <= maxPrettyPrintLevel {
s := obj.ToString().String()
fmt.Fprintf(ctx.w, "<%s %s>", obj.ClassName(), s)
} else {
fmt.Fprintf(ctx.w, "<%s>", obj.ClassName())
}
}
}
func (ctx ppctx) fields(obj *goja.Object) []string {
var (
vals, methods []string
seen = make(map[string]bool)
)
add := func(k string) {
if seen[k] || boringKeys[k] || strings.HasPrefix(k, "_") {
return
}
seen[k] = true
key := SafeGet(obj, k)
if key == nil {
// The value corresponding to that key could not be found
// (typically because it is backed by an RPC call that is
// not supported by this instance. Add it to the list of
// values so that it appears as `undefined` to the user.
vals = append(vals, k)
} else {
if _, callable := goja.AssertFunction(key); callable {
methods = append(methods, k)
} else {
vals = append(vals, k)
}
}
}
iterOwnAndConstructorKeys(ctx.vm, obj, add)
sort.Strings(vals)
sort.Strings(methods)
return append(vals, methods...)
}
func iterOwnAndConstructorKeys(vm *goja.Runtime, obj *goja.Object, f func(string)) {
seen := make(map[string]bool)
iterOwnKeys(vm, obj, func(prop string) {
seen[prop] = true
f(prop)
})
if cp := constructorPrototype(vm, obj); cp != nil {
iterOwnKeys(vm, cp, func(prop string) {
if !seen[prop] {
f(prop)
}
})
}
}
func iterOwnKeys(vm *goja.Runtime, obj *goja.Object, f func(string)) {
Object := vm.Get("Object").ToObject(vm)
getOwnPropertyNames, isFunc := goja.AssertFunction(Object.Get("getOwnPropertyNames"))
if !isFunc {
panic(vm.ToValue("Object.getOwnPropertyNames isn't a function"))
}
rv, err := getOwnPropertyNames(goja.Null(), obj)
if err != nil {
panic(vm.ToValue(fmt.Sprintf("Error getting object properties: %v", err)))
}
gv := rv.Export()
switch gv := gv.(type) {
case []interface{}:
for _, v := range gv {
f(v.(string))
}
case []string:
for _, v := range gv {
f(v)
}
default:
panic(fmt.Errorf("Object.getOwnPropertyNames returned unexpected type %T", gv))
}
}
func (ctx ppctx) isBigNumber(v *goja.Object) bool {
// Handle numbers with custom constructor.
if obj := v.Get("constructor").ToObject(ctx.vm); obj != nil {
if strings.HasPrefix(toString(obj), "function BigNumber") {
return true
}
}
// Handle default constructor.
BigNumber := ctx.vm.Get("BigNumber").ToObject(ctx.vm)
if BigNumber == nil {
return false
}
prototype := BigNumber.Get("prototype").ToObject(ctx.vm)
isPrototypeOf, callable := goja.AssertFunction(prototype.Get("isPrototypeOf"))
if !callable {
return false
}
bv, _ := isPrototypeOf(prototype, v)
return bv.ToBoolean()
}
func toString(obj *goja.Object) string {
return obj.ToString().String()
}
func constructorPrototype(vm *goja.Runtime, obj *goja.Object) *goja.Object {
if v := obj.Get("constructor"); v != nil {
if v := v.ToObject(vm).Get("prototype"); v != nil {
return v.ToObject(vm)
}
}
return nil
}

View file

@ -26,8 +26,8 @@ import (
"sync"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/console/prompt"
"github.com/ethereum/go-ethereum/internal/ethapi"
"github.com/ethereum/go-ethereum/internal/prompt"
"github.com/ethereum/go-ethereum/log"
)