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