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