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