github.com/aigarnetwork/aigar@v0.0.0-20191115204914-d59a6eb70f8e/console/bridge.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // Copyright 2019 The go-aigar Authors 3 // This file is part of the go-aigar library. 4 // 5 // The go-aigar library is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Lesser General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // The go-aigar library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Lesser General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public License 16 // along with the go-aigar library. If not, see <http://www.gnu.org/licenses/>. 17 18 package console 19 20 import ( 21 "encoding/json" 22 "fmt" 23 "io" 24 "strings" 25 "time" 26 27 "github.com/AigarNetwork/aigar/accounts/scwallet" 28 "github.com/AigarNetwork/aigar/accounts/usbwallet" 29 "github.com/AigarNetwork/aigar/log" 30 "github.com/AigarNetwork/aigar/rpc" 31 "github.com/robertkrimen/otto" 32 ) 33 34 // bridge is a collection of JavaScript utility methods to bride the .js runtime 35 // environment and the Go RPC connection backing the remote method calls. 36 type bridge struct { 37 client *rpc.Client // RPC client to execute Ethereum requests through 38 prompter UserPrompter // Input prompter to allow interactive user feedback 39 printer io.Writer // Output writer to serialize any display strings to 40 } 41 42 // newBridge creates a new JavaScript wrapper around an RPC client. 43 func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer) *bridge { 44 return &bridge{ 45 client: client, 46 prompter: prompter, 47 printer: printer, 48 } 49 } 50 51 // NewAccount is a wrapper around the personal.newAccount RPC method that uses a 52 // non-echoing password prompt to acquire the passphrase and executes the original 53 // RPC method (saved in jeth.newAccount) with it to actually execute the RPC call. 54 func (b *bridge) NewAccount(call otto.FunctionCall) (response otto.Value) { 55 var ( 56 password string 57 confirm string 58 err error 59 ) 60 switch { 61 // No password was specified, prompt the user for it 62 case len(call.ArgumentList) == 0: 63 if password, err = b.prompter.PromptPassword("Password: "); err != nil { 64 throwJSException(err.Error()) 65 } 66 if confirm, err = b.prompter.PromptPassword("Repeat password: "); err != nil { 67 throwJSException(err.Error()) 68 } 69 if password != confirm { 70 throwJSException("passwords don't match!") 71 } 72 73 // A single string password was specified, use that 74 case len(call.ArgumentList) == 1 && call.Argument(0).IsString(): 75 password, _ = call.Argument(0).ToString() 76 77 // Otherwise fail with some error 78 default: 79 throwJSException("expected 0 or 1 string argument") 80 } 81 // Password acquired, execute the call and return 82 ret, err := call.Otto.Call("jeth.newAccount", nil, password) 83 if err != nil { 84 throwJSException(err.Error()) 85 } 86 return ret 87 } 88 89 // OpenWallet is a wrapper around personal.openWallet which can interpret and 90 // react to certain error messages, such as the Trezor PIN matrix request. 91 func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) { 92 // Make sure we have a wallet specified to open 93 if !call.Argument(0).IsString() { 94 throwJSException("first argument must be the wallet URL to open") 95 } 96 wallet := call.Argument(0) 97 98 var passwd otto.Value 99 if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() { 100 passwd, _ = otto.ToValue("") 101 } else { 102 passwd = call.Argument(1) 103 } 104 // Open the wallet and return if successful in itself 105 val, err := call.Otto.Call("jeth.openWallet", nil, wallet, passwd) 106 if err == nil { 107 return val 108 } 109 110 // Wallet open failed, report error unless it's a PIN or PUK entry 111 switch { 112 case strings.HasSuffix(err.Error(), usbwallet.ErrTrezorPINNeeded.Error()): 113 val, err = b.readPinAndReopenWallet(call) 114 if err == nil { 115 return val 116 } 117 val, err = b.readPassphraseAndReopenWallet(call) 118 if err != nil { 119 throwJSException(err.Error()) 120 } 121 122 case strings.HasSuffix(err.Error(), scwallet.ErrPairingPasswordNeeded.Error()): 123 // PUK input requested, fetch from the user and call open again 124 if input, err := b.prompter.PromptPassword("Please enter the pairing password: "); err != nil { 125 throwJSException(err.Error()) 126 } else { 127 passwd, _ = otto.ToValue(input) 128 } 129 if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { 130 if !strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()) { 131 throwJSException(err.Error()) 132 } else { 133 // PIN input requested, fetch from the user and call open again 134 if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { 135 throwJSException(err.Error()) 136 } else { 137 passwd, _ = otto.ToValue(input) 138 } 139 if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { 140 throwJSException(err.Error()) 141 } 142 } 143 } 144 145 case strings.HasSuffix(err.Error(), scwallet.ErrPINUnblockNeeded.Error()): 146 // PIN unblock requested, fetch PUK and new PIN from the user 147 var pukpin string 148 if input, err := b.prompter.PromptPassword("Please enter current PUK: "); err != nil { 149 throwJSException(err.Error()) 150 } else { 151 pukpin = input 152 } 153 if input, err := b.prompter.PromptPassword("Please enter new PIN: "); err != nil { 154 throwJSException(err.Error()) 155 } else { 156 pukpin += input 157 } 158 passwd, _ = otto.ToValue(pukpin) 159 if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { 160 throwJSException(err.Error()) 161 } 162 163 case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()): 164 // PIN input requested, fetch from the user and call open again 165 if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { 166 throwJSException(err.Error()) 167 } else { 168 passwd, _ = otto.ToValue(input) 169 } 170 if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { 171 throwJSException(err.Error()) 172 } 173 174 default: 175 // Unknown error occurred, drop to the user 176 throwJSException(err.Error()) 177 } 178 return val 179 } 180 181 func (b *bridge) readPassphraseAndReopenWallet(call otto.FunctionCall) (otto.Value, error) { 182 var passwd otto.Value 183 wallet := call.Argument(0) 184 if input, err := b.prompter.PromptPassword("Please enter your password: "); err != nil { 185 throwJSException(err.Error()) 186 } else { 187 passwd, _ = otto.ToValue(input) 188 } 189 return call.Otto.Call("jeth.openWallet", nil, wallet, passwd) 190 } 191 192 func (b *bridge) readPinAndReopenWallet(call otto.FunctionCall) (otto.Value, error) { 193 var passwd otto.Value 194 wallet := call.Argument(0) 195 // Trezor PIN matrix input requested, display the matrix to the user and fetch the data 196 fmt.Fprintf(b.printer, "Look at the device for number positions\n\n") 197 fmt.Fprintf(b.printer, "7 | 8 | 9\n") 198 fmt.Fprintf(b.printer, "--+---+--\n") 199 fmt.Fprintf(b.printer, "4 | 5 | 6\n") 200 fmt.Fprintf(b.printer, "--+---+--\n") 201 fmt.Fprintf(b.printer, "1 | 2 | 3\n\n") 202 203 if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { 204 throwJSException(err.Error()) 205 } else { 206 passwd, _ = otto.ToValue(input) 207 } 208 return call.Otto.Call("jeth.openWallet", nil, wallet, passwd) 209 } 210 211 // UnlockAccount is a wrapper around the personal.unlockAccount RPC method that 212 // uses a non-echoing password prompt to acquire the passphrase and executes the 213 // original RPC method (saved in jeth.unlockAccount) with it to actually execute 214 // the RPC call. 215 func (b *bridge) UnlockAccount(call otto.FunctionCall) (response otto.Value) { 216 // Make sure we have an account specified to unlock 217 if !call.Argument(0).IsString() { 218 throwJSException("first argument must be the account to unlock") 219 } 220 account := call.Argument(0) 221 222 // If password is not given or is the null value, prompt the user for it 223 var passwd otto.Value 224 225 if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() { 226 fmt.Fprintf(b.printer, "Unlock account %s\n", account) 227 if input, err := b.prompter.PromptPassword("Password: "); err != nil { 228 throwJSException(err.Error()) 229 } else { 230 passwd, _ = otto.ToValue(input) 231 } 232 } else { 233 if !call.Argument(1).IsString() { 234 throwJSException("password must be a string") 235 } 236 passwd = call.Argument(1) 237 } 238 // Third argument is the duration how long the account must be unlocked. 239 duration := otto.NullValue() 240 if call.Argument(2).IsDefined() && !call.Argument(2).IsNull() { 241 if !call.Argument(2).IsNumber() { 242 throwJSException("unlock duration must be a number") 243 } 244 duration = call.Argument(2) 245 } 246 // Send the request to the backend and return 247 val, err := call.Otto.Call("jeth.unlockAccount", nil, account, passwd, duration) 248 if err != nil { 249 throwJSException(err.Error()) 250 } 251 return val 252 } 253 254 // Sign is a wrapper around the personal.sign RPC method that uses a non-echoing password 255 // prompt to acquire the passphrase and executes the original RPC method (saved in 256 // jeth.sign) with it to actually execute the RPC call. 257 func (b *bridge) Sign(call otto.FunctionCall) (response otto.Value) { 258 var ( 259 message = call.Argument(0) 260 account = call.Argument(1) 261 passwd = call.Argument(2) 262 ) 263 264 if !message.IsString() { 265 throwJSException("first argument must be the message to sign") 266 } 267 if !account.IsString() { 268 throwJSException("second argument must be the account to sign with") 269 } 270 271 // if the password is not given or null ask the user and ensure password is a string 272 if passwd.IsUndefined() || passwd.IsNull() { 273 fmt.Fprintf(b.printer, "Give password for account %s\n", account) 274 if input, err := b.prompter.PromptPassword("Password: "); err != nil { 275 throwJSException(err.Error()) 276 } else { 277 passwd, _ = otto.ToValue(input) 278 } 279 } 280 if !passwd.IsString() { 281 throwJSException("third argument must be the password to unlock the account") 282 } 283 284 // Send the request to the backend and return 285 val, err := call.Otto.Call("jeth.sign", nil, message, account, passwd) 286 if err != nil { 287 throwJSException(err.Error()) 288 } 289 return val 290 } 291 292 // Sleep will block the console for the specified number of seconds. 293 func (b *bridge) Sleep(call otto.FunctionCall) (response otto.Value) { 294 if call.Argument(0).IsNumber() { 295 sleep, _ := call.Argument(0).ToInteger() 296 time.Sleep(time.Duration(sleep) * time.Second) 297 return otto.TrueValue() 298 } 299 return throwJSException("usage: sleep(<number of seconds>)") 300 } 301 302 // SleepBlocks will block the console for a specified number of new blocks optionally 303 // until the given timeout is reached. 304 func (b *bridge) SleepBlocks(call otto.FunctionCall) (response otto.Value) { 305 var ( 306 blocks = int64(0) 307 sleep = int64(9999999999999999) // indefinitely 308 ) 309 // Parse the input parameters for the sleep 310 nArgs := len(call.ArgumentList) 311 if nArgs == 0 { 312 throwJSException("usage: sleepBlocks(<n blocks>[, max sleep in seconds])") 313 } 314 if nArgs >= 1 { 315 if call.Argument(0).IsNumber() { 316 blocks, _ = call.Argument(0).ToInteger() 317 } else { 318 throwJSException("expected number as first argument") 319 } 320 } 321 if nArgs >= 2 { 322 if call.Argument(1).IsNumber() { 323 sleep, _ = call.Argument(1).ToInteger() 324 } else { 325 throwJSException("expected number as second argument") 326 } 327 } 328 // go through the console, this will allow web3 to call the appropriate 329 // callbacks if a delayed response or notification is received. 330 blockNumber := func() int64 { 331 result, err := call.Otto.Run("eth.blockNumber") 332 if err != nil { 333 throwJSException(err.Error()) 334 } 335 block, err := result.ToInteger() 336 if err != nil { 337 throwJSException(err.Error()) 338 } 339 return block 340 } 341 // Poll the current block number until either it ot a timeout is reached 342 targetBlockNr := blockNumber() + blocks 343 deadline := time.Now().Add(time.Duration(sleep) * time.Second) 344 345 for time.Now().Before(deadline) { 346 if blockNumber() >= targetBlockNr { 347 return otto.TrueValue() 348 } 349 time.Sleep(time.Second) 350 } 351 return otto.FalseValue() 352 } 353 354 type jsonrpcCall struct { 355 ID int64 356 Method string 357 Params []interface{} 358 } 359 360 // Send implements the web3 provider "send" method. 361 func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) { 362 // Remarshal the request into a Go value. 363 JSON, _ := call.Otto.Object("JSON") 364 reqVal, err := JSON.Call("stringify", call.Argument(0)) 365 if err != nil { 366 throwJSException(err.Error()) 367 } 368 var ( 369 rawReq = reqVal.String() 370 dec = json.NewDecoder(strings.NewReader(rawReq)) 371 reqs []jsonrpcCall 372 batch bool 373 ) 374 dec.UseNumber() // avoid float64s 375 if rawReq[0] == '[' { 376 batch = true 377 dec.Decode(&reqs) 378 } else { 379 batch = false 380 reqs = make([]jsonrpcCall, 1) 381 dec.Decode(&reqs[0]) 382 } 383 384 // Execute the requests. 385 resps, _ := call.Otto.Object("new Array()") 386 for _, req := range reqs { 387 resp, _ := call.Otto.Object(`({"jsonrpc":"2.0"})`) 388 resp.Set("id", req.ID) 389 var result json.RawMessage 390 err = b.client.Call(&result, req.Method, req.Params...) 391 switch err := err.(type) { 392 case nil: 393 if result == nil { 394 // Special case null because it is decoded as an empty 395 // raw message for some reason. 396 resp.Set("result", otto.NullValue()) 397 } else { 398 resultVal, err := JSON.Call("parse", string(result)) 399 if err != nil { 400 setError(resp, -32603, err.Error()) 401 } else { 402 resp.Set("result", resultVal) 403 } 404 } 405 case rpc.Error: 406 setError(resp, err.ErrorCode(), err.Error()) 407 default: 408 setError(resp, -32603, err.Error()) 409 } 410 resps.Call("push", resp) 411 } 412 413 // Return the responses either to the callback (if supplied) 414 // or directly as the return value. 415 if batch { 416 response = resps.Value() 417 } else { 418 response, _ = resps.Get("0") 419 } 420 if fn := call.Argument(1); fn.Class() == "Function" { 421 fn.Call(otto.NullValue(), otto.NullValue(), response) 422 return otto.UndefinedValue() 423 } 424 return response 425 } 426 427 func setError(resp *otto.Object, code int, msg string) { 428 resp.Set("error", map[string]interface{}{"code": code, "message": msg}) 429 } 430 431 // throwJSException panics on an otto.Value. The Otto VM will recover from the 432 // Go panic and throw msg as a JavaScript error. 433 func throwJSException(msg interface{}) otto.Value { 434 val, err := otto.ToValue(msg) 435 if err != nil { 436 log.Error("Failed to serialize JavaScript exception", "exception", msg, "err", err) 437 } 438 panic(val) 439 }