github.com/theQRL/go-zond@v0.1.1/cmd/clef/main.go (about) 1 // Copyright 2018 The go-ethereum Authors 2 // This file is part of go-ethereum. 3 // 4 // go-ethereum is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU 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 // go-ethereum 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 General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. 16 17 package main 18 19 import ( 20 "bufio" 21 "context" 22 "crypto/rand" 23 "crypto/sha256" 24 "encoding/hex" 25 "encoding/json" 26 "errors" 27 "fmt" 28 "io" 29 "math/big" 30 "net" 31 "os" 32 "os/signal" 33 "path/filepath" 34 "runtime" 35 "strings" 36 "time" 37 38 "github.com/theQRL/go-zond/accounts" 39 "github.com/theQRL/go-zond/accounts/keystore" 40 "github.com/theQRL/go-zond/cmd/utils" 41 "github.com/theQRL/go-zond/common" 42 "github.com/theQRL/go-zond/common/hexutil" 43 "github.com/theQRL/go-zond/core/types" 44 "github.com/theQRL/go-zond/crypto" 45 "github.com/theQRL/go-zond/internal/ethapi" 46 "github.com/theQRL/go-zond/internal/flags" 47 "github.com/theQRL/go-zond/log" 48 "github.com/theQRL/go-zond/node" 49 "github.com/theQRL/go-zond/params" 50 "github.com/theQRL/go-zond/rlp" 51 "github.com/theQRL/go-zond/rpc" 52 "github.com/theQRL/go-zond/signer/core" 53 "github.com/theQRL/go-zond/signer/core/apitypes" 54 "github.com/theQRL/go-zond/signer/fourbyte" 55 "github.com/theQRL/go-zond/signer/rules" 56 "github.com/theQRL/go-zond/signer/storage" 57 "github.com/mattn/go-colorable" 58 "github.com/mattn/go-isatty" 59 "github.com/urfave/cli/v2" 60 ) 61 62 const legalWarning = ` 63 WARNING! 64 65 Clef is an account management tool. It may, like any software, contain bugs. 66 67 Please take care to 68 - backup your keystore files, 69 - verify that the keystore(s) can be opened with your password. 70 71 Clef is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 72 without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 73 PURPOSE. See the GNU General Public License for more details. 74 ` 75 76 var ( 77 logLevelFlag = &cli.IntFlag{ 78 Name: "loglevel", 79 Value: 3, 80 Usage: "log level to emit to the screen", 81 } 82 advancedMode = &cli.BoolFlag{ 83 Name: "advanced", 84 Usage: "If enabled, issues warnings instead of rejections for suspicious requests. Default off", 85 } 86 acceptFlag = &cli.BoolFlag{ 87 Name: "suppress-bootwarn", 88 Usage: "If set, does not show the warning during boot", 89 } 90 keystoreFlag = &cli.StringFlag{ 91 Name: "keystore", 92 Value: filepath.Join(node.DefaultDataDir(), "keystore"), 93 Usage: "Directory for the keystore", 94 } 95 configdirFlag = &cli.StringFlag{ 96 Name: "configdir", 97 Value: DefaultConfigDir(), 98 Usage: "Directory for Clef configuration", 99 } 100 chainIdFlag = &cli.Int64Flag{ 101 Name: "chainid", 102 Value: params.MainnetChainConfig.ChainID.Int64(), 103 Usage: "Chain id to use for signing (1=mainnet, 5=Goerli)", 104 } 105 rpcPortFlag = &cli.IntFlag{ 106 Name: "http.port", 107 Usage: "HTTP-RPC server listening port", 108 Value: node.DefaultHTTPPort + 5, 109 Category: flags.APICategory, 110 } 111 signerSecretFlag = &cli.StringFlag{ 112 Name: "signersecret", 113 Usage: "A file containing the (encrypted) master seed to encrypt Clef data, e.g. keystore credentials and ruleset hash", 114 } 115 customDBFlag = &cli.StringFlag{ 116 Name: "4bytedb-custom", 117 Usage: "File used for writing new 4byte-identifiers submitted via API", 118 Value: "./4byte-custom.json", 119 } 120 auditLogFlag = &cli.StringFlag{ 121 Name: "auditlog", 122 Usage: "File used to emit audit logs. Set to \"\" to disable", 123 Value: "audit.log", 124 } 125 ruleFlag = &cli.StringFlag{ 126 Name: "rules", 127 Usage: "Path to the rule file to auto-authorize requests with", 128 } 129 stdiouiFlag = &cli.BoolFlag{ 130 Name: "stdio-ui", 131 Usage: "Use STDIN/STDOUT as a channel for an external UI. " + 132 "This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " + 133 "interface, and can be used when Clef is started by an external process.", 134 } 135 testFlag = &cli.BoolFlag{ 136 Name: "stdio-ui-test", 137 Usage: "Mechanism to test interface between Clef and UI. Requires 'stdio-ui'.", 138 } 139 initCommand = &cli.Command{ 140 Action: initializeSecrets, 141 Name: "init", 142 Usage: "Initialize the signer, generate secret storage", 143 ArgsUsage: "", 144 Flags: []cli.Flag{ 145 logLevelFlag, 146 configdirFlag, 147 }, 148 Description: ` 149 The init command generates a master seed which Clef can use to store credentials and data needed for 150 the rule-engine to work.`, 151 } 152 attestCommand = &cli.Command{ 153 Action: attestFile, 154 Name: "attest", 155 Usage: "Attest that a js-file is to be used", 156 ArgsUsage: "<sha256sum>", 157 Flags: []cli.Flag{ 158 logLevelFlag, 159 configdirFlag, 160 signerSecretFlag, 161 }, 162 Description: ` 163 The attest command stores the sha256 of the rule.js-file that you want to use for automatic processing of 164 incoming requests. 165 166 Whenever you make an edit to the rule file, you need to use attestation to tell 167 Clef that the file is 'safe' to execute.`, 168 } 169 setCredentialCommand = &cli.Command{ 170 Action: setCredential, 171 Name: "setpw", 172 Usage: "Store a credential for a keystore file", 173 ArgsUsage: "<address>", 174 Flags: []cli.Flag{ 175 logLevelFlag, 176 configdirFlag, 177 signerSecretFlag, 178 }, 179 Description: ` 180 The setpw command stores a password for a given address (keyfile). 181 `} 182 delCredentialCommand = &cli.Command{ 183 Action: removeCredential, 184 Name: "delpw", 185 Usage: "Remove a credential for a keystore file", 186 ArgsUsage: "<address>", 187 Flags: []cli.Flag{ 188 logLevelFlag, 189 configdirFlag, 190 signerSecretFlag, 191 }, 192 Description: ` 193 The delpw command removes a password for a given address (keyfile). 194 `} 195 newAccountCommand = &cli.Command{ 196 Action: newAccount, 197 Name: "newaccount", 198 Usage: "Create a new account", 199 ArgsUsage: "", 200 Flags: []cli.Flag{ 201 logLevelFlag, 202 keystoreFlag, 203 utils.LightKDFFlag, 204 acceptFlag, 205 }, 206 Description: ` 207 The newaccount command creates a new keystore-backed account. It is a convenience-method 208 which can be used in lieu of an external UI. 209 `} 210 gendocCommand = &cli.Command{ 211 Action: GenDoc, 212 Name: "gendoc", 213 Usage: "Generate documentation about json-rpc format", 214 Description: ` 215 The gendoc generates example structures of the json-rpc communication types. 216 `} 217 listAccountsCommand = &cli.Command{ 218 Action: listAccounts, 219 Name: "list-accounts", 220 Usage: "List accounts in the keystore", 221 Flags: []cli.Flag{ 222 logLevelFlag, 223 keystoreFlag, 224 utils.LightKDFFlag, 225 acceptFlag, 226 }, 227 Description: ` 228 Lists the accounts in the keystore. 229 `} 230 listWalletsCommand = &cli.Command{ 231 Action: listWallets, 232 Name: "list-wallets", 233 Usage: "List wallets known to Clef", 234 Flags: []cli.Flag{ 235 logLevelFlag, 236 keystoreFlag, 237 utils.LightKDFFlag, 238 acceptFlag, 239 }, 240 Description: ` 241 Lists the wallets known to Clef. 242 `} 243 importRawCommand = &cli.Command{ 244 Action: accountImport, 245 Name: "importraw", 246 Usage: "Import a hex-encoded private key.", 247 ArgsUsage: "<keyfile>", 248 Flags: []cli.Flag{ 249 logLevelFlag, 250 keystoreFlag, 251 utils.LightKDFFlag, 252 acceptFlag, 253 }, 254 Description: ` 255 Imports an unencrypted private key from <keyfile> and creates a new account. 256 Prints the address. 257 The keyfile is assumed to contain an unencrypted private key in hexadecimal format. 258 The account is saved in encrypted format, you are prompted for a password. 259 `} 260 ) 261 262 var app = flags.NewApp("Manage Ethereum account operations") 263 264 func init() { 265 app.Name = "Clef" 266 app.Flags = []cli.Flag{ 267 logLevelFlag, 268 keystoreFlag, 269 configdirFlag, 270 chainIdFlag, 271 utils.LightKDFFlag, 272 utils.NoUSBFlag, 273 utils.SmartCardDaemonPathFlag, 274 utils.HTTPListenAddrFlag, 275 utils.HTTPVirtualHostsFlag, 276 utils.IPCDisabledFlag, 277 utils.IPCPathFlag, 278 utils.HTTPEnabledFlag, 279 rpcPortFlag, 280 signerSecretFlag, 281 customDBFlag, 282 auditLogFlag, 283 ruleFlag, 284 stdiouiFlag, 285 testFlag, 286 advancedMode, 287 acceptFlag, 288 } 289 app.Action = signer 290 app.Commands = []*cli.Command{initCommand, 291 attestCommand, 292 setCredentialCommand, 293 delCredentialCommand, 294 newAccountCommand, 295 importRawCommand, 296 gendocCommand, 297 listAccountsCommand, 298 listWalletsCommand, 299 } 300 } 301 302 func main() { 303 if err := app.Run(os.Args); err != nil { 304 fmt.Fprintln(os.Stderr, err) 305 os.Exit(1) 306 } 307 } 308 309 func initializeSecrets(c *cli.Context) error { 310 // Get past the legal message 311 if err := initialize(c); err != nil { 312 return err 313 } 314 // Ensure the master key does not yet exist, we're not willing to overwrite 315 configDir := c.String(configdirFlag.Name) 316 if err := os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) { 317 return err 318 } 319 location := filepath.Join(configDir, "masterseed.json") 320 if _, err := os.Stat(location); err == nil { 321 return fmt.Errorf("master key %v already exists, will not overwrite", location) 322 } 323 // Key file does not exist yet, generate a new one and encrypt it 324 masterSeed := make([]byte, 256) 325 num, err := io.ReadFull(rand.Reader, masterSeed) 326 if err != nil { 327 return err 328 } 329 if num != len(masterSeed) { 330 return errors.New("failed to read enough random") 331 } 332 n, p := keystore.StandardScryptN, keystore.StandardScryptP 333 if c.Bool(utils.LightKDFFlag.Name) { 334 n, p = keystore.LightScryptN, keystore.LightScryptP 335 } 336 text := "The master seed of clef will be locked with a password.\nPlease specify a password. Do not forget this password!" 337 var password string 338 for { 339 password = utils.GetPassPhrase(text, true) 340 if err := core.ValidatePasswordFormat(password); err != nil { 341 fmt.Printf("invalid password: %v\n", err) 342 } else { 343 fmt.Println() 344 break 345 } 346 } 347 cipherSeed, err := encryptSeed(masterSeed, []byte(password), n, p) 348 if err != nil { 349 return fmt.Errorf("failed to encrypt master seed: %v", err) 350 } 351 // Double check the master key path to ensure nothing wrote there in between 352 if err = os.Mkdir(configDir, 0700); err != nil && !os.IsExist(err) { 353 return err 354 } 355 if _, err := os.Stat(location); err == nil { 356 return fmt.Errorf("master key %v already exists, will not overwrite", location) 357 } 358 // Write the file and print the usual warning message 359 if err = os.WriteFile(location, cipherSeed, 0400); err != nil { 360 return err 361 } 362 fmt.Printf("A master seed has been generated into %s\n", location) 363 fmt.Printf(` 364 This is required to be able to store credentials, such as: 365 * Passwords for keystores (used by rule engine) 366 * Storage for JavaScript auto-signing rules 367 * Hash of JavaScript rule-file 368 369 You should treat 'masterseed.json' with utmost secrecy and make a backup of it! 370 * The password is necessary but not enough, you need to back up the master seed too! 371 * The master seed does not contain your accounts, those need to be backed up separately! 372 373 `) 374 return nil 375 } 376 377 func attestFile(ctx *cli.Context) error { 378 if ctx.NArg() < 1 { 379 utils.Fatalf("This command requires an argument.") 380 } 381 if err := initialize(ctx); err != nil { 382 return err 383 } 384 385 stretchedKey, err := readMasterKey(ctx, nil) 386 if err != nil { 387 utils.Fatalf(err.Error()) 388 } 389 configDir := ctx.String(configdirFlag.Name) 390 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 391 confKey := crypto.Keccak256([]byte("config"), stretchedKey) 392 393 // Initialize the encrypted storages 394 configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confKey) 395 val := ctx.Args().First() 396 configStorage.Put("ruleset_sha256", val) 397 log.Info("Ruleset attestation updated", "sha256", val) 398 return nil 399 } 400 401 func initInternalApi(c *cli.Context) (*core.UIServerAPI, core.UIClientAPI, error) { 402 if err := initialize(c); err != nil { 403 return nil, nil, err 404 } 405 var ( 406 ui = core.NewCommandlineUI() 407 pwStorage storage.Storage = &storage.NoStorage{} 408 ksLoc = c.String(keystoreFlag.Name) 409 lightKdf = c.Bool(utils.LightKDFFlag.Name) 410 ) 411 am := core.StartClefAccountManager(ksLoc, true, lightKdf, "") 412 api := core.NewSignerAPI(am, 0, true, ui, nil, false, pwStorage) 413 internalApi := core.NewUIServerAPI(api) 414 return internalApi, ui, nil 415 } 416 417 func setCredential(ctx *cli.Context) error { 418 if ctx.NArg() < 1 { 419 utils.Fatalf("This command requires an address to be passed as an argument") 420 } 421 if err := initialize(ctx); err != nil { 422 return err 423 } 424 addr := ctx.Args().First() 425 if !common.IsHexAddress(addr) { 426 utils.Fatalf("Invalid address specified: %s", addr) 427 } 428 address := common.HexToAddress(addr) 429 password := utils.GetPassPhrase("Please enter a password to store for this address:", true) 430 fmt.Println() 431 432 stretchedKey, err := readMasterKey(ctx, nil) 433 if err != nil { 434 utils.Fatalf(err.Error()) 435 } 436 configDir := ctx.String(configdirFlag.Name) 437 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 438 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 439 440 pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 441 pwStorage.Put(address.Hex(), password) 442 443 log.Info("Credential store updated", "set", address) 444 return nil 445 } 446 447 func removeCredential(ctx *cli.Context) error { 448 if ctx.NArg() < 1 { 449 utils.Fatalf("This command requires an address to be passed as an argument") 450 } 451 if err := initialize(ctx); err != nil { 452 return err 453 } 454 addr := ctx.Args().First() 455 if !common.IsHexAddress(addr) { 456 utils.Fatalf("Invalid address specified: %s", addr) 457 } 458 address := common.HexToAddress(addr) 459 460 stretchedKey, err := readMasterKey(ctx, nil) 461 if err != nil { 462 utils.Fatalf(err.Error()) 463 } 464 configDir := ctx.String(configdirFlag.Name) 465 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 466 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 467 468 pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 469 pwStorage.Del(address.Hex()) 470 471 log.Info("Credential store updated", "unset", address) 472 return nil 473 } 474 475 func initialize(c *cli.Context) error { 476 // Set up the logger to print everything 477 logOutput := os.Stdout 478 if c.Bool(stdiouiFlag.Name) { 479 logOutput = os.Stderr 480 // If using the stdioui, we can't do the 'confirm'-flow 481 if !c.Bool(acceptFlag.Name) { 482 fmt.Fprint(logOutput, legalWarning) 483 } 484 } else if !c.Bool(acceptFlag.Name) { 485 if !confirm(legalWarning) { 486 return errors.New("aborted by user") 487 } 488 fmt.Println() 489 } 490 usecolor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb" 491 output := io.Writer(logOutput) 492 if usecolor { 493 output = colorable.NewColorable(logOutput) 494 } 495 log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int(logLevelFlag.Name)), log.StreamHandler(output, log.TerminalFormat(usecolor)))) 496 497 return nil 498 } 499 500 func newAccount(c *cli.Context) error { 501 internalApi, _, err := initInternalApi(c) 502 if err != nil { 503 return err 504 } 505 addr, err := internalApi.New(context.Background()) 506 if err == nil { 507 fmt.Printf("Generated account %v\n", addr.String()) 508 } 509 return err 510 } 511 512 func listAccounts(c *cli.Context) error { 513 internalApi, _, err := initInternalApi(c) 514 if err != nil { 515 return err 516 } 517 accs, err := internalApi.ListAccounts(context.Background()) 518 if err != nil { 519 return err 520 } 521 if len(accs) == 0 { 522 fmt.Println("\nThe keystore is empty.") 523 } 524 fmt.Println() 525 for _, account := range accs { 526 fmt.Printf("%v (%v)\n", account.Address, account.URL) 527 } 528 return err 529 } 530 531 func listWallets(c *cli.Context) error { 532 internalApi, _, err := initInternalApi(c) 533 if err != nil { 534 return err 535 } 536 wallets := internalApi.ListWallets() 537 if len(wallets) == 0 { 538 fmt.Println("\nThere are no wallets.") 539 } 540 fmt.Println() 541 for i, wallet := range wallets { 542 fmt.Printf("- Wallet %d at %v (%v %v)\n", i, wallet.URL, wallet.Status, wallet.Failure) 543 for j, acc := range wallet.Accounts { 544 fmt.Printf(" -Account %d: %v (%v)\n", j, acc.Address, acc.URL) 545 } 546 fmt.Println() 547 } 548 return nil 549 } 550 551 // accountImport imports a raw hexadecimal private key via CLI. 552 func accountImport(c *cli.Context) error { 553 if c.Args().Len() != 1 { 554 return errors.New("<keyfile> must be given as first argument.") 555 } 556 internalApi, ui, err := initInternalApi(c) 557 if err != nil { 558 return err 559 } 560 pKey, err := crypto.LoadECDSA(c.Args().First()) 561 if err != nil { 562 return err 563 } 564 readPw := func(prompt string) (string, error) { 565 resp, err := ui.OnInputRequired(core.UserInputRequest{ 566 Title: "Password", 567 Prompt: prompt, 568 IsPassword: true, 569 }) 570 if err != nil { 571 return "", err 572 } 573 return resp.Text, nil 574 } 575 first, err := readPw("Please enter a password for the imported account") 576 if err != nil { 577 return err 578 } 579 second, err := readPw("Please repeat the password you just entered") 580 if err != nil { 581 return err 582 } 583 if first != second { 584 return errors.New("Passwords do not match") 585 } 586 acc, err := internalApi.ImportRawKey(hex.EncodeToString(crypto.FromECDSA(pKey)), first) 587 if err != nil { 588 return err 589 } 590 ui.ShowInfo(fmt.Sprintf(`Key imported: 591 Address %v 592 Keystore file: %v 593 594 The key is now encrypted; losing the password will result in permanently losing 595 access to the key and all associated funds! 596 597 Make sure to backup keystore and passwords in a safe location.`, 598 acc.Address, acc.URL.Path)) 599 return nil 600 } 601 602 // ipcEndpoint resolves an IPC endpoint based on a configured value, taking into 603 // account the set data folders as well as the designated platform we're currently 604 // running on. 605 func ipcEndpoint(ipcPath, datadir string) string { 606 // On windows we can only use plain top-level pipes 607 if runtime.GOOS == "windows" { 608 if strings.HasPrefix(ipcPath, `\\.\pipe\`) { 609 return ipcPath 610 } 611 return `\\.\pipe\` + ipcPath 612 } 613 // Resolve names into the data directory full paths otherwise 614 if filepath.Base(ipcPath) == ipcPath { 615 if datadir == "" { 616 return filepath.Join(os.TempDir(), ipcPath) 617 } 618 return filepath.Join(datadir, ipcPath) 619 } 620 return ipcPath 621 } 622 623 func signer(c *cli.Context) error { 624 // If we have some unrecognized command, bail out 625 if c.NArg() > 0 { 626 return fmt.Errorf("invalid command: %q", c.Args().First()) 627 } 628 if err := initialize(c); err != nil { 629 return err 630 } 631 var ( 632 ui core.UIClientAPI 633 ) 634 if c.Bool(stdiouiFlag.Name) { 635 log.Info("Using stdin/stdout as UI-channel") 636 ui = core.NewStdIOUI() 637 } else { 638 log.Info("Using CLI as UI-channel") 639 ui = core.NewCommandlineUI() 640 } 641 // 4bytedb data 642 fourByteLocal := c.String(customDBFlag.Name) 643 db, err := fourbyte.NewWithFile(fourByteLocal) 644 if err != nil { 645 utils.Fatalf(err.Error()) 646 } 647 embeds, locals := db.Size() 648 log.Info("Loaded 4byte database", "embeds", embeds, "locals", locals, "local", fourByteLocal) 649 650 var ( 651 api core.ExternalAPI 652 pwStorage storage.Storage = &storage.NoStorage{} 653 ) 654 configDir := c.String(configdirFlag.Name) 655 if stretchedKey, err := readMasterKey(c, ui); err != nil { 656 log.Warn("Failed to open master, rules disabled", "err", err) 657 } else { 658 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 659 660 // Generate domain specific keys 661 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 662 jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey) 663 confkey := crypto.Keccak256([]byte("config"), stretchedKey) 664 665 // Initialize the encrypted storages 666 pwStorage = storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 667 jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey) 668 configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey) 669 670 // Do we have a rule-file? 671 if ruleFile := c.String(ruleFlag.Name); ruleFile != "" { 672 ruleJS, err := os.ReadFile(ruleFile) 673 if err != nil { 674 log.Warn("Could not load rules, disabling", "file", ruleFile, "err", err) 675 } else { 676 shasum := sha256.Sum256(ruleJS) 677 foundShaSum := hex.EncodeToString(shasum[:]) 678 storedShasum, _ := configStorage.Get("ruleset_sha256") 679 if storedShasum != foundShaSum { 680 log.Warn("Rule hash not attested, disabling", "hash", foundShaSum, "attested", storedShasum) 681 } else { 682 // Initialize rules 683 ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage) 684 if err != nil { 685 utils.Fatalf(err.Error()) 686 } 687 ruleEngine.Init(string(ruleJS)) 688 ui = ruleEngine 689 log.Info("Rule engine configured", "file", c.String(ruleFlag.Name)) 690 } 691 } 692 } 693 } 694 var ( 695 chainId = c.Int64(chainIdFlag.Name) 696 ksLoc = c.String(keystoreFlag.Name) 697 lightKdf = c.Bool(utils.LightKDFFlag.Name) 698 advanced = c.Bool(advancedMode.Name) 699 nousb = c.Bool(utils.NoUSBFlag.Name) 700 scpath = c.String(utils.SmartCardDaemonPathFlag.Name) 701 ) 702 log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc, 703 "light-kdf", lightKdf, "advanced", advanced) 704 am := core.StartClefAccountManager(ksLoc, nousb, lightKdf, scpath) 705 apiImpl := core.NewSignerAPI(am, chainId, nousb, ui, db, advanced, pwStorage) 706 707 // Establish the bidirectional communication, by creating a new UI backend and registering 708 // it with the UI. 709 ui.RegisterUIServer(core.NewUIServerAPI(apiImpl)) 710 api = apiImpl 711 712 // Audit logging 713 if logfile := c.String(auditLogFlag.Name); logfile != "" { 714 api, err = core.NewAuditLogger(logfile, api) 715 if err != nil { 716 utils.Fatalf(err.Error()) 717 } 718 log.Info("Audit logs configured", "file", logfile) 719 } 720 // register signer API with server 721 var ( 722 extapiURL = "n/a" 723 ipcapiURL = "n/a" 724 ) 725 rpcAPI := []rpc.API{ 726 { 727 Namespace: "account", 728 Service: api, 729 }, 730 } 731 if c.Bool(utils.HTTPEnabledFlag.Name) { 732 vhosts := utils.SplitAndTrim(c.String(utils.HTTPVirtualHostsFlag.Name)) 733 cors := utils.SplitAndTrim(c.String(utils.HTTPCORSDomainFlag.Name)) 734 735 srv := rpc.NewServer() 736 srv.SetBatchLimits(node.DefaultConfig.BatchRequestLimit, node.DefaultConfig.BatchResponseMaxSize) 737 err := node.RegisterApis(rpcAPI, []string{"account"}, srv) 738 if err != nil { 739 utils.Fatalf("Could not register API: %w", err) 740 } 741 handler := node.NewHTTPHandlerStack(srv, cors, vhosts, nil) 742 743 // set port 744 port := c.Int(rpcPortFlag.Name) 745 746 // start http server 747 httpEndpoint := net.JoinHostPort(c.String(utils.HTTPListenAddrFlag.Name), fmt.Sprintf("%d", port)) 748 httpServer, addr, err := node.StartHTTPEndpoint(httpEndpoint, rpc.DefaultHTTPTimeouts, handler) 749 if err != nil { 750 utils.Fatalf("Could not start RPC api: %v", err) 751 } 752 extapiURL = fmt.Sprintf("http://%v/", addr) 753 log.Info("HTTP endpoint opened", "url", extapiURL) 754 755 defer func() { 756 // Don't bother imposing a timeout here. 757 httpServer.Shutdown(context.Background()) 758 log.Info("HTTP endpoint closed", "url", extapiURL) 759 }() 760 } 761 if !c.Bool(utils.IPCDisabledFlag.Name) { 762 givenPath := c.String(utils.IPCPathFlag.Name) 763 ipcapiURL = ipcEndpoint(filepath.Join(givenPath, "clef.ipc"), configDir) 764 listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI) 765 if err != nil { 766 utils.Fatalf("Could not start IPC api: %v", err) 767 } 768 log.Info("IPC endpoint opened", "url", ipcapiURL) 769 defer func() { 770 listener.Close() 771 log.Info("IPC endpoint closed", "url", ipcapiURL) 772 }() 773 } 774 if c.Bool(testFlag.Name) { 775 log.Info("Performing UI test") 776 go testExternalUI(apiImpl) 777 } 778 ui.OnSignerStartup(core.StartupInfo{ 779 Info: map[string]interface{}{ 780 "intapi_version": core.InternalAPIVersion, 781 "extapi_version": core.ExternalAPIVersion, 782 "extapi_http": extapiURL, 783 "extapi_ipc": ipcapiURL, 784 }}) 785 786 abortChan := make(chan os.Signal, 1) 787 signal.Notify(abortChan, os.Interrupt) 788 789 sig := <-abortChan 790 log.Info("Exiting...", "signal", sig) 791 792 return nil 793 } 794 795 // DefaultConfigDir is the default config directory to use for the vaults and other 796 // persistence requirements. 797 func DefaultConfigDir() string { 798 // Try to place the data folder in the user's home dir 799 home := flags.HomeDir() 800 if home != "" { 801 if runtime.GOOS == "darwin" { 802 return filepath.Join(home, "Library", "Signer") 803 } else if runtime.GOOS == "windows" { 804 appdata := os.Getenv("APPDATA") 805 if appdata != "" { 806 return filepath.Join(appdata, "Signer") 807 } 808 return filepath.Join(home, "AppData", "Roaming", "Signer") 809 } 810 return filepath.Join(home, ".clef") 811 } 812 // As we cannot guess a stable location, return empty and handle later 813 return "" 814 } 815 816 func readMasterKey(ctx *cli.Context, ui core.UIClientAPI) ([]byte, error) { 817 var ( 818 file string 819 configDir = ctx.String(configdirFlag.Name) 820 ) 821 if ctx.IsSet(signerSecretFlag.Name) { 822 file = ctx.String(signerSecretFlag.Name) 823 } else { 824 file = filepath.Join(configDir, "masterseed.json") 825 } 826 if err := checkFile(file); err != nil { 827 return nil, err 828 } 829 cipherKey, err := os.ReadFile(file) 830 if err != nil { 831 return nil, err 832 } 833 var password string 834 // If ui is not nil, get the password from ui. 835 if ui != nil { 836 resp, err := ui.OnInputRequired(core.UserInputRequest{ 837 Title: "Master Password", 838 Prompt: "Please enter the password to decrypt the master seed", 839 IsPassword: true}) 840 if err != nil { 841 return nil, err 842 } 843 password = resp.Text 844 } else { 845 password = utils.GetPassPhrase("Decrypt master seed of clef", false) 846 } 847 masterSeed, err := decryptSeed(cipherKey, password) 848 if err != nil { 849 return nil, errors.New("failed to decrypt the master seed of clef") 850 } 851 if len(masterSeed) < 256 { 852 return nil, fmt.Errorf("master seed of insufficient length, expected >255 bytes, got %d", len(masterSeed)) 853 } 854 // Create vault location 855 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterSeed)[:10])) 856 err = os.Mkdir(vaultLocation, 0700) 857 if err != nil && !os.IsExist(err) { 858 return nil, err 859 } 860 return masterSeed, nil 861 } 862 863 // checkFile is a convenience function to check if a file 864 // * exists 865 // * is mode 0400 (unix only) 866 func checkFile(filename string) error { 867 info, err := os.Stat(filename) 868 if err != nil { 869 return fmt.Errorf("failed stat on %s: %v", filename, err) 870 } 871 // Check the unix permission bits 872 // However, on windows, we cannot use the unix perm-bits, see 873 // https://github.com/theQRL/go-zond/issues/20123 874 if runtime.GOOS != "windows" && info.Mode().Perm()&0377 != 0 { 875 return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String()) 876 } 877 return nil 878 } 879 880 // confirm displays a text and asks for user confirmation 881 func confirm(text string) bool { 882 fmt.Print(text) 883 fmt.Printf("\nEnter 'ok' to proceed:\n> ") 884 885 text, err := bufio.NewReader(os.Stdin).ReadString('\n') 886 if err != nil { 887 log.Crit("Failed to read user input", "err", err) 888 } 889 if text := strings.TrimSpace(text); text == "ok" { 890 return true 891 } 892 return false 893 } 894 895 func testExternalUI(api *core.SignerAPI) { 896 ctx := context.WithValue(context.Background(), "remote", "clef binary") 897 ctx = context.WithValue(ctx, "scheme", "in-proc") 898 ctx = context.WithValue(ctx, "local", "main") 899 errs := make([]string, 0) 900 901 a := common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") 902 addErr := func(errStr string) { 903 log.Info("Test error", "err", errStr) 904 errs = append(errs, errStr) 905 } 906 907 queryUser := func(q string) string { 908 resp, err := api.UI.OnInputRequired(core.UserInputRequest{ 909 Title: "Testing", 910 Prompt: q, 911 }) 912 if err != nil { 913 addErr(err.Error()) 914 } 915 return resp.Text 916 } 917 expectResponse := func(testcase, question, expect string) { 918 if got := queryUser(question); got != expect { 919 addErr(fmt.Sprintf("%s: got %v, expected %v", testcase, got, expect)) 920 } 921 } 922 expectApprove := func(testcase string, err error) { 923 if err == nil || err == accounts.ErrUnknownAccount { 924 return 925 } 926 addErr(fmt.Sprintf("%v: expected no error, got %v", testcase, err.Error())) 927 } 928 expectDeny := func(testcase string, err error) { 929 if err == nil || err != core.ErrRequestDenied { 930 addErr(fmt.Sprintf("%v: expected ErrRequestDenied, got %v", testcase, err)) 931 } 932 } 933 var delay = 1 * time.Second 934 // Test display of info and error 935 { 936 api.UI.ShowInfo("If you see this message, enter 'yes' to next question") 937 time.Sleep(delay) 938 expectResponse("showinfo", "Did you see the message? [yes/no]", "yes") 939 api.UI.ShowError("If you see this message, enter 'yes' to the next question") 940 time.Sleep(delay) 941 expectResponse("showerror", "Did you see the message? [yes/no]", "yes") 942 } 943 { // Sign data test - clique header 944 api.UI.ShowInfo("Please approve the next request for signing a clique header") 945 time.Sleep(delay) 946 cliqueHeader := types.Header{ 947 ParentHash: common.HexToHash("0000H45H"), 948 UncleHash: common.HexToHash("0000H45H"), 949 Coinbase: common.HexToAddress("0000H45H"), 950 Root: common.HexToHash("0000H00H"), 951 TxHash: common.HexToHash("0000H45H"), 952 ReceiptHash: common.HexToHash("0000H45H"), 953 Difficulty: big.NewInt(1337), 954 Number: big.NewInt(1337), 955 GasLimit: 1338, 956 GasUsed: 1338, 957 Time: 1338, 958 Extra: []byte("Extra data Extra data Extra data Extra data Extra data Extra data Extra data Extra data"), 959 MixDigest: common.HexToHash("0x0000H45H"), 960 } 961 cliqueRlp, err := rlp.EncodeToBytes(cliqueHeader) 962 if err != nil { 963 utils.Fatalf("Should not error: %v", err) 964 } 965 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 966 _, err = api.SignData(ctx, accounts.MimetypeClique, *addr, hexutil.Encode(cliqueRlp)) 967 expectApprove("signdata - clique header", err) 968 } 969 { // Sign data test - typed data 970 api.UI.ShowInfo("Please approve the next request for signing EIP-712 typed data") 971 time.Sleep(delay) 972 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 973 data := `{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Person":[{"name":"name","type":"string"},{"name":"test","type":"uint8"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","domain":{"name":"Ether Mail","version":"1","chainId":"1","verifyingContract":"0xCCCcccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","test":"3","wallet":"0xcD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","test":"2"},"contents":"Hello, Bob!"}}` 974 //_, err := api.SignData(ctx, accounts.MimetypeTypedData, *addr, hexutil.Encode([]byte(data))) 975 var typedData apitypes.TypedData 976 json.Unmarshal([]byte(data), &typedData) 977 _, err := api.SignTypedData(ctx, *addr, typedData) 978 expectApprove("sign 712 typed data", err) 979 } 980 { // Sign data test - plain text 981 api.UI.ShowInfo("Please approve the next request for signing text") 982 time.Sleep(delay) 983 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 984 _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world"))) 985 expectApprove("signdata - text", err) 986 } 987 { // Sign data test - plain text reject 988 api.UI.ShowInfo("Please deny the next request for signing text") 989 time.Sleep(delay) 990 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 991 _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world"))) 992 expectDeny("signdata - text", err) 993 } 994 { // Sign transaction 995 api.UI.ShowInfo("Please reject next transaction") 996 time.Sleep(delay) 997 data := hexutil.Bytes([]byte{}) 998 to := common.NewMixedcaseAddress(a) 999 tx := apitypes.SendTxArgs{ 1000 Data: &data, 1001 Nonce: 0x1, 1002 Value: hexutil.Big(*big.NewInt(6)), 1003 From: common.NewMixedcaseAddress(a), 1004 To: &to, 1005 GasPrice: (*hexutil.Big)(big.NewInt(5)), 1006 Gas: 1000, 1007 Input: nil, 1008 } 1009 _, err := api.SignTransaction(ctx, tx, nil) 1010 expectDeny("signtransaction [1]", err) 1011 expectResponse("signtransaction [2]", "Did you see any warnings for the last transaction? (yes/no)", "no") 1012 } 1013 { // Listing 1014 api.UI.ShowInfo("Please reject listing-request") 1015 time.Sleep(delay) 1016 _, err := api.List(ctx) 1017 expectDeny("list", err) 1018 } 1019 { // Import 1020 api.UI.ShowInfo("Please reject new account-request") 1021 time.Sleep(delay) 1022 _, err := api.New(ctx) 1023 expectDeny("newaccount", err) 1024 } 1025 { // Metadata 1026 api.UI.ShowInfo("Please check if you see the Origin in next listing (approve or deny)") 1027 time.Sleep(delay) 1028 api.List(context.WithValue(ctx, "Origin", "origin.com")) 1029 expectResponse("metadata - origin", "Did you see origin (origin.com)? [yes/no] ", "yes") 1030 } 1031 1032 for _, e := range errs { 1033 log.Error(e) 1034 } 1035 result := fmt.Sprintf("Tests completed. %d errors:\n%s\n", len(errs), strings.Join(errs, "\n")) 1036 api.UI.ShowInfo(result) 1037 } 1038 1039 type encryptedSeedStorage struct { 1040 Description string `json:"description"` 1041 Version int `json:"version"` 1042 Params keystore.CryptoJSON `json:"params"` 1043 } 1044 1045 // encryptSeed uses a similar scheme as the keystore uses, but with a different wrapping, 1046 // to encrypt the master seed 1047 func encryptSeed(seed []byte, auth []byte, scryptN, scryptP int) ([]byte, error) { 1048 cryptoStruct, err := keystore.EncryptDataV3(seed, auth, scryptN, scryptP) 1049 if err != nil { 1050 return nil, err 1051 } 1052 return json.Marshal(&encryptedSeedStorage{"Clef seed", 1, cryptoStruct}) 1053 } 1054 1055 // decryptSeed decrypts the master seed 1056 func decryptSeed(keyjson []byte, auth string) ([]byte, error) { 1057 var encSeed encryptedSeedStorage 1058 if err := json.Unmarshal(keyjson, &encSeed); err != nil { 1059 return nil, err 1060 } 1061 if encSeed.Version != 1 { 1062 log.Warn(fmt.Sprintf("unsupported encryption format of seed: %d, operation will likely fail", encSeed.Version)) 1063 } 1064 seed, err := keystore.DecryptDataV3(encSeed.Params, auth) 1065 if err != nil { 1066 return nil, err 1067 } 1068 return seed, err 1069 } 1070 1071 // GenDoc outputs examples of all structures used in json-rpc communication 1072 func GenDoc(ctx *cli.Context) error { 1073 var ( 1074 a = common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") 1075 b = common.HexToAddress("0x1111111122222222222233333333334444444444") 1076 meta = core.Metadata{ 1077 Scheme: "http", 1078 Local: "localhost:8545", 1079 Origin: "www.malicious.ru", 1080 Remote: "localhost:9999", 1081 UserAgent: "Firefox 3.2", 1082 } 1083 output []string 1084 add = func(name, desc string, v interface{}) { 1085 if data, err := json.MarshalIndent(v, "", " "); err == nil { 1086 output = append(output, fmt.Sprintf("### %s\n\n%s\n\nExample:\n```json\n%s\n```", name, desc, data)) 1087 } else { 1088 log.Error("Error generating output", "err", err) 1089 } 1090 } 1091 ) 1092 1093 { // Sign plain text request 1094 desc := "SignDataRequest contains information about a pending request to sign some data. " + 1095 "The data to be signed can be of various types, defined by content-type. Clef has done most " + 1096 "of the work in canonicalizing and making sense of the data, and it's up to the UI to present" + 1097 "the user with the contents of the `message`" 1098 sighash, msg := accounts.TextAndHash([]byte("hello world")) 1099 messages := []*apitypes.NameValueType{{Name: "message", Value: msg, Typ: accounts.MimetypeTextPlain}} 1100 1101 add("SignDataRequest", desc, &core.SignDataRequest{ 1102 Address: common.NewMixedcaseAddress(a), 1103 Meta: meta, 1104 ContentType: accounts.MimetypeTextPlain, 1105 Rawdata: []byte(msg), 1106 Messages: messages, 1107 Hash: sighash}) 1108 } 1109 { // Sign plain text response 1110 add("SignDataResponse - approve", "Response to SignDataRequest", 1111 &core.SignDataResponse{Approved: true}) 1112 add("SignDataResponse - deny", "Response to SignDataRequest", 1113 &core.SignDataResponse{}) 1114 } 1115 { // Sign transaction request 1116 desc := "SignTxRequest contains information about a pending request to sign a transaction. " + 1117 "Aside from the transaction itself, there is also a `call_info`-struct. That struct contains " + 1118 "messages of various types, that the user should be informed of." + 1119 "\n\n" + 1120 "As in any request, it's important to consider that the `meta` info also contains untrusted data." + 1121 "\n\n" + 1122 "The `transaction` (on input into clef) can have either `data` or `input` -- if both are set, " + 1123 "they must be identical, otherwise an error is generated. " + 1124 "However, Clef will always use `data` when passing this struct on (if Clef does otherwise, please file a ticket)" 1125 1126 data := hexutil.Bytes([]byte{0x01, 0x02, 0x03, 0x04}) 1127 add("SignTxRequest", desc, &core.SignTxRequest{ 1128 Meta: meta, 1129 Callinfo: []apitypes.ValidationInfo{ 1130 {Typ: "Warning", Message: "Something looks odd, show this message as a warning"}, 1131 {Typ: "Info", Message: "User should see this as well"}, 1132 }, 1133 Transaction: apitypes.SendTxArgs{ 1134 Data: &data, 1135 Nonce: 0x1, 1136 Value: hexutil.Big(*big.NewInt(6)), 1137 From: common.NewMixedcaseAddress(a), 1138 To: nil, 1139 GasPrice: (*hexutil.Big)(big.NewInt(5)), 1140 Gas: 1000, 1141 Input: nil, 1142 }}) 1143 } 1144 { // Sign tx response 1145 data := hexutil.Bytes([]byte{0x04, 0x03, 0x02, 0x01}) 1146 add("SignTxResponse - approve", "Response to request to sign a transaction. This response needs to contain the `transaction`"+ 1147 ", because the UI is free to make modifications to the transaction.", 1148 &core.SignTxResponse{Approved: true, 1149 Transaction: apitypes.SendTxArgs{ 1150 Data: &data, 1151 Nonce: 0x4, 1152 Value: hexutil.Big(*big.NewInt(6)), 1153 From: common.NewMixedcaseAddress(a), 1154 To: nil, 1155 GasPrice: (*hexutil.Big)(big.NewInt(5)), 1156 Gas: 1000, 1157 Input: nil, 1158 }}) 1159 add("SignTxResponse - deny", "Response to SignTxRequest. When denying a request, there's no need to "+ 1160 "provide the transaction in return", 1161 &core.SignTxResponse{}) 1162 } 1163 { // WHen a signed tx is ready to go out 1164 desc := "SignTransactionResult is used in the call `clef` -> `OnApprovedTx(result)`" + 1165 "\n\n" + 1166 "This occurs _after_ successful completion of the entire signing procedure, but right before the signed " + 1167 "transaction is passed to the external caller. This method (and data) can be used by the UI to signal " + 1168 "to the user that the transaction was signed, but it is primarily useful for ruleset implementations." + 1169 "\n\n" + 1170 "A ruleset that implements a rate limitation needs to know what transactions are sent out to the external " + 1171 "interface. By hooking into this methods, the ruleset can maintain track of that count." + 1172 "\n\n" + 1173 "**OBS:** Note that if an attacker can restore your `clef` data to a previous point in time" + 1174 " (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content. " + 1175 "\n\n" + 1176 "The `OnApproved` method cannot be responded to, it's purely informative" 1177 1178 rlpdata := common.FromHex("0xf85d640101948a8eafb1cf62bfbeb1741769dae1a9dd47996192018026a0716bd90515acb1e68e5ac5867aa11a1e65399c3349d479f5fb698554ebc6f293a04e8a4ebfff434e971e0ef12c5bf3a881b06fd04fc3f8b8a7291fb67a26a1d4ed") 1179 var tx types.Transaction 1180 tx.UnmarshalBinary(rlpdata) 1181 add("OnApproved - SignTransactionResult", desc, ðapi.SignTransactionResult{Raw: rlpdata, Tx: &tx}) 1182 } 1183 { // User input 1184 add("UserInputRequest", "Sent when clef needs the user to provide data. If 'password' is true, the input field should be treated accordingly (echo-free)", 1185 &core.UserInputRequest{IsPassword: true, Title: "The title here", Prompt: "The question to ask the user"}) 1186 add("UserInputResponse", "Response to UserInputRequest", 1187 &core.UserInputResponse{Text: "The textual response from user"}) 1188 } 1189 { // List request 1190 add("ListRequest", "Sent when a request has been made to list addresses. The UI is provided with the "+ 1191 "full `account`s, including local directory names. Note: this information is not passed back to the external caller, "+ 1192 "who only sees the `address`es. ", 1193 &core.ListRequest{ 1194 Meta: meta, 1195 Accounts: []accounts.Account{ 1196 {Address: a, URL: accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/a"}}, 1197 {Address: b, URL: accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/b"}}}, 1198 }) 1199 1200 add("ListResponse", "Response to list request. The response contains a list of all addresses to show to the caller. "+ 1201 "Note: the UI is free to respond with any address the caller, regardless of whether it exists or not", 1202 &core.ListResponse{ 1203 Accounts: []accounts.Account{ 1204 { 1205 Address: common.HexToAddress("0xcowbeef000000cowbeef00000000000000000c0w"), 1206 URL: accounts.URL{Path: ".. ignored .."}, 1207 }, 1208 { 1209 Address: common.HexToAddress("0xffffffffffffffffffffffffffffffffffffffff"), 1210 }, 1211 }}) 1212 } 1213 1214 fmt.Println(`## UI Client interface 1215 1216 These data types are defined in the channel between clef and the UI`) 1217 for _, elem := range output { 1218 fmt.Println(elem) 1219 } 1220 return nil 1221 }