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