github.1485827954.workers.dev/ethereum/go-ethereum@v1.14.3/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/ethereum/go-ethereum/accounts" 39 "github.com/ethereum/go-ethereum/accounts/keystore" 40 "github.com/ethereum/go-ethereum/cmd/utils" 41 "github.com/ethereum/go-ethereum/common" 42 "github.com/ethereum/go-ethereum/common/hexutil" 43 "github.com/ethereum/go-ethereum/core/types" 44 "github.com/ethereum/go-ethereum/crypto" 45 "github.com/ethereum/go-ethereum/internal/ethapi" 46 "github.com/ethereum/go-ethereum/internal/flags" 47 "github.com/ethereum/go-ethereum/log" 48 "github.com/ethereum/go-ethereum/node" 49 "github.com/ethereum/go-ethereum/params" 50 "github.com/ethereum/go-ethereum/rlp" 51 "github.com/ethereum/go-ethereum/rpc" 52 "github.com/ethereum/go-ethereum/signer/core" 53 "github.com/ethereum/go-ethereum/signer/core/apitypes" 54 "github.com/ethereum/go-ethereum/signer/fourbyte" 55 "github.com/ethereum/go-ethereum/signer/rules" 56 "github.com/ethereum/go-ethereum/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 verbosity := log.FromLegacyLevel(c.Int(logLevelFlag.Name)) 496 log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(output, verbosity, usecolor))) 497 498 return nil 499 } 500 501 func newAccount(c *cli.Context) error { 502 internalApi, _, err := initInternalApi(c) 503 if err != nil { 504 return err 505 } 506 addr, err := internalApi.New(context.Background()) 507 if err == nil { 508 fmt.Printf("Generated account %v\n", addr.String()) 509 } 510 return err 511 } 512 513 func listAccounts(c *cli.Context) error { 514 internalApi, _, err := initInternalApi(c) 515 if err != nil { 516 return err 517 } 518 accs, err := internalApi.ListAccounts(context.Background()) 519 if err != nil { 520 return err 521 } 522 if len(accs) == 0 { 523 fmt.Println("\nThe keystore is empty.") 524 } 525 fmt.Println() 526 for _, account := range accs { 527 fmt.Printf("%v (%v)\n", account.Address, account.URL) 528 } 529 return err 530 } 531 532 func listWallets(c *cli.Context) error { 533 internalApi, _, err := initInternalApi(c) 534 if err != nil { 535 return err 536 } 537 wallets := internalApi.ListWallets() 538 if len(wallets) == 0 { 539 fmt.Println("\nThere are no wallets.") 540 } 541 fmt.Println() 542 for i, wallet := range wallets { 543 fmt.Printf("- Wallet %d at %v (%v %v)\n", i, wallet.URL, wallet.Status, wallet.Failure) 544 for j, acc := range wallet.Accounts { 545 fmt.Printf(" -Account %d: %v (%v)\n", j, acc.Address, acc.URL) 546 } 547 fmt.Println() 548 } 549 return nil 550 } 551 552 // accountImport imports a raw hexadecimal private key via CLI. 553 func accountImport(c *cli.Context) error { 554 if c.Args().Len() != 1 { 555 return errors.New("<keyfile> must be given as first argument.") 556 } 557 internalApi, ui, err := initInternalApi(c) 558 if err != nil { 559 return err 560 } 561 pKey, err := crypto.LoadECDSA(c.Args().First()) 562 if err != nil { 563 return err 564 } 565 readPw := func(prompt string) (string, error) { 566 resp, err := ui.OnInputRequired(core.UserInputRequest{ 567 Title: "Password", 568 Prompt: prompt, 569 IsPassword: true, 570 }) 571 if err != nil { 572 return "", err 573 } 574 return resp.Text, nil 575 } 576 first, err := readPw("Please enter a password for the imported account") 577 if err != nil { 578 return err 579 } 580 second, err := readPw("Please repeat the password you just entered") 581 if err != nil { 582 return err 583 } 584 if first != second { 585 //lint:ignore ST1005 This is a message for the user 586 return errors.New("Passwords do not match") 587 } 588 acc, err := internalApi.ImportRawKey(hex.EncodeToString(crypto.FromECDSA(pKey)), first) 589 if err != nil { 590 return err 591 } 592 ui.ShowInfo(fmt.Sprintf(`Key imported: 593 Address %v 594 Keystore file: %v 595 596 The key is now encrypted; losing the password will result in permanently losing 597 access to the key and all associated funds! 598 599 Make sure to backup keystore and passwords in a safe location.`, 600 acc.Address, acc.URL.Path)) 601 return nil 602 } 603 604 // ipcEndpoint resolves an IPC endpoint based on a configured value, taking into 605 // account the set data folders as well as the designated platform we're currently 606 // running on. 607 func ipcEndpoint(ipcPath, datadir string) string { 608 // On windows we can only use plain top-level pipes 609 if runtime.GOOS == "windows" { 610 if strings.HasPrefix(ipcPath, `\\.\pipe\`) { 611 return ipcPath 612 } 613 return `\\.\pipe\` + ipcPath 614 } 615 // Resolve names into the data directory full paths otherwise 616 if filepath.Base(ipcPath) == ipcPath { 617 if datadir == "" { 618 return filepath.Join(os.TempDir(), ipcPath) 619 } 620 return filepath.Join(datadir, ipcPath) 621 } 622 return ipcPath 623 } 624 625 func signer(c *cli.Context) error { 626 // If we have some unrecognized command, bail out 627 if c.NArg() > 0 { 628 return fmt.Errorf("invalid command: %q", c.Args().First()) 629 } 630 if err := initialize(c); err != nil { 631 return err 632 } 633 var ( 634 ui core.UIClientAPI 635 ) 636 if c.Bool(stdiouiFlag.Name) { 637 log.Info("Using stdin/stdout as UI-channel") 638 ui = core.NewStdIOUI() 639 } else { 640 log.Info("Using CLI as UI-channel") 641 ui = core.NewCommandlineUI() 642 } 643 // 4bytedb data 644 fourByteLocal := c.String(customDBFlag.Name) 645 db, err := fourbyte.NewWithFile(fourByteLocal) 646 if err != nil { 647 utils.Fatalf(err.Error()) 648 } 649 embeds, locals := db.Size() 650 log.Info("Loaded 4byte database", "embeds", embeds, "locals", locals, "local", fourByteLocal) 651 652 var ( 653 api core.ExternalAPI 654 pwStorage storage.Storage = &storage.NoStorage{} 655 ) 656 configDir := c.String(configdirFlag.Name) 657 if stretchedKey, err := readMasterKey(c, ui); err != nil { 658 log.Warn("Failed to open master, rules disabled", "err", err) 659 } else { 660 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10])) 661 662 // Generate domain specific keys 663 pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey) 664 jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey) 665 confkey := crypto.Keccak256([]byte("config"), stretchedKey) 666 667 // Initialize the encrypted storages 668 pwStorage = storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey) 669 jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey) 670 configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey) 671 672 // Do we have a rule-file? 673 if ruleFile := c.String(ruleFlag.Name); ruleFile != "" { 674 ruleJS, err := os.ReadFile(ruleFile) 675 if err != nil { 676 log.Warn("Could not load rules, disabling", "file", ruleFile, "err", err) 677 } else { 678 shasum := sha256.Sum256(ruleJS) 679 foundShaSum := hex.EncodeToString(shasum[:]) 680 storedShasum, _ := configStorage.Get("ruleset_sha256") 681 if storedShasum != foundShaSum { 682 log.Warn("Rule hash not attested, disabling", "hash", foundShaSum, "attested", storedShasum) 683 } else { 684 // Initialize rules 685 ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage) 686 if err != nil { 687 utils.Fatalf(err.Error()) 688 } 689 ruleEngine.Init(string(ruleJS)) 690 ui = ruleEngine 691 log.Info("Rule engine configured", "file", c.String(ruleFlag.Name)) 692 } 693 } 694 } 695 } 696 var ( 697 chainId = c.Int64(chainIdFlag.Name) 698 ksLoc = c.String(keystoreFlag.Name) 699 lightKdf = c.Bool(utils.LightKDFFlag.Name) 700 advanced = c.Bool(advancedMode.Name) 701 nousb = c.Bool(utils.NoUSBFlag.Name) 702 scpath = c.String(utils.SmartCardDaemonPathFlag.Name) 703 ) 704 log.Info("Starting signer", "chainid", chainId, "keystore", ksLoc, 705 "light-kdf", lightKdf, "advanced", advanced) 706 am := core.StartClefAccountManager(ksLoc, nousb, lightKdf, scpath) 707 defer am.Close() 708 apiImpl := core.NewSignerAPI(am, chainId, nousb, ui, db, advanced, pwStorage) 709 710 // Establish the bidirectional communication, by creating a new UI backend and registering 711 // it with the UI. 712 ui.RegisterUIServer(core.NewUIServerAPI(apiImpl)) 713 api = apiImpl 714 715 // Audit logging 716 if logfile := c.String(auditLogFlag.Name); logfile != "" { 717 api, err = core.NewAuditLogger(logfile, api) 718 if err != nil { 719 utils.Fatalf(err.Error()) 720 } 721 log.Info("Audit logs configured", "file", logfile) 722 } 723 // register signer API with server 724 var ( 725 extapiURL = "n/a" 726 ipcapiURL = "n/a" 727 ) 728 rpcAPI := []rpc.API{ 729 { 730 Namespace: "account", 731 Service: api, 732 }, 733 } 734 if c.Bool(utils.HTTPEnabledFlag.Name) { 735 vhosts := utils.SplitAndTrim(c.String(utils.HTTPVirtualHostsFlag.Name)) 736 cors := utils.SplitAndTrim(c.String(utils.HTTPCORSDomainFlag.Name)) 737 738 srv := rpc.NewServer() 739 srv.SetBatchLimits(node.DefaultConfig.BatchRequestLimit, node.DefaultConfig.BatchResponseMaxSize) 740 err := node.RegisterApis(rpcAPI, []string{"account"}, srv) 741 if err != nil { 742 utils.Fatalf("Could not register API: %w", err) 743 } 744 handler := node.NewHTTPHandlerStack(srv, cors, vhosts, nil) 745 746 // set port 747 port := c.Int(rpcPortFlag.Name) 748 749 // start http server 750 httpEndpoint := net.JoinHostPort(c.String(utils.HTTPListenAddrFlag.Name), fmt.Sprintf("%d", port)) 751 httpServer, addr, err := node.StartHTTPEndpoint(httpEndpoint, rpc.DefaultHTTPTimeouts, handler) 752 if err != nil { 753 utils.Fatalf("Could not start RPC api: %v", err) 754 } 755 extapiURL = fmt.Sprintf("http://%v/", addr) 756 log.Info("HTTP endpoint opened", "url", extapiURL) 757 758 defer func() { 759 // Don't bother imposing a timeout here. 760 httpServer.Shutdown(context.Background()) 761 log.Info("HTTP endpoint closed", "url", extapiURL) 762 }() 763 } 764 if !c.Bool(utils.IPCDisabledFlag.Name) { 765 givenPath := c.String(utils.IPCPathFlag.Name) 766 ipcapiURL = ipcEndpoint(filepath.Join(givenPath, "clef.ipc"), configDir) 767 listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI) 768 if err != nil { 769 utils.Fatalf("Could not start IPC api: %v", err) 770 } 771 log.Info("IPC endpoint opened", "url", ipcapiURL) 772 defer func() { 773 listener.Close() 774 log.Info("IPC endpoint closed", "url", ipcapiURL) 775 }() 776 } 777 if c.Bool(testFlag.Name) { 778 log.Info("Performing UI test") 779 go testExternalUI(apiImpl) 780 } 781 ui.OnSignerStartup(core.StartupInfo{ 782 Info: map[string]interface{}{ 783 "intapi_version": core.InternalAPIVersion, 784 "extapi_version": core.ExternalAPIVersion, 785 "extapi_http": extapiURL, 786 "extapi_ipc": ipcapiURL, 787 }}) 788 789 abortChan := make(chan os.Signal, 1) 790 signal.Notify(abortChan, os.Interrupt) 791 792 sig := <-abortChan 793 log.Info("Exiting...", "signal", sig) 794 795 return nil 796 } 797 798 // DefaultConfigDir is the default config directory to use for the vaults and other 799 // persistence requirements. 800 func DefaultConfigDir() string { 801 // Try to place the data folder in the user's home dir 802 home := flags.HomeDir() 803 if home != "" { 804 if runtime.GOOS == "darwin" { 805 return filepath.Join(home, "Library", "Signer") 806 } else if runtime.GOOS == "windows" { 807 appdata := os.Getenv("APPDATA") 808 if appdata != "" { 809 return filepath.Join(appdata, "Signer") 810 } 811 return filepath.Join(home, "AppData", "Roaming", "Signer") 812 } 813 return filepath.Join(home, ".clef") 814 } 815 // As we cannot guess a stable location, return empty and handle later 816 return "" 817 } 818 819 func readMasterKey(ctx *cli.Context, ui core.UIClientAPI) ([]byte, error) { 820 var ( 821 file string 822 configDir = ctx.String(configdirFlag.Name) 823 ) 824 if ctx.IsSet(signerSecretFlag.Name) { 825 file = ctx.String(signerSecretFlag.Name) 826 } else { 827 file = filepath.Join(configDir, "masterseed.json") 828 } 829 if err := checkFile(file); err != nil { 830 return nil, err 831 } 832 cipherKey, err := os.ReadFile(file) 833 if err != nil { 834 return nil, err 835 } 836 var password string 837 // If ui is not nil, get the password from ui. 838 if ui != nil { 839 resp, err := ui.OnInputRequired(core.UserInputRequest{ 840 Title: "Master Password", 841 Prompt: "Please enter the password to decrypt the master seed", 842 IsPassword: true}) 843 if err != nil { 844 return nil, err 845 } 846 password = resp.Text 847 } else { 848 password = utils.GetPassPhrase("Decrypt master seed of clef", false) 849 } 850 masterSeed, err := decryptSeed(cipherKey, password) 851 if err != nil { 852 return nil, errors.New("failed to decrypt the master seed of clef") 853 } 854 if len(masterSeed) < 256 { 855 return nil, fmt.Errorf("master seed of insufficient length, expected >255 bytes, got %d", len(masterSeed)) 856 } 857 // Create vault location 858 vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterSeed)[:10])) 859 err = os.Mkdir(vaultLocation, 0700) 860 if err != nil && !os.IsExist(err) { 861 return nil, err 862 } 863 return masterSeed, nil 864 } 865 866 // checkFile is a convenience function to check if a file 867 // * exists 868 // * is mode 0400 (unix only) 869 func checkFile(filename string) error { 870 info, err := os.Stat(filename) 871 if err != nil { 872 return fmt.Errorf("failed stat on %s: %v", filename, err) 873 } 874 // Check the unix permission bits 875 // However, on windows, we cannot use the unix perm-bits, see 876 // https://github.com/ethereum/go-ethereum/issues/20123 877 if runtime.GOOS != "windows" && info.Mode().Perm()&0377 != 0 { 878 return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String()) 879 } 880 return nil 881 } 882 883 // confirm displays a text and asks for user confirmation 884 func confirm(text string) bool { 885 fmt.Print(text) 886 fmt.Printf("\nEnter 'ok' to proceed:\n> ") 887 888 text, err := bufio.NewReader(os.Stdin).ReadString('\n') 889 if err != nil { 890 log.Crit("Failed to read user input", "err", err) 891 } 892 if text := strings.TrimSpace(text); text == "ok" { 893 return true 894 } 895 return false 896 } 897 898 func testExternalUI(api *core.SignerAPI) { 899 ctx := context.WithValue(context.Background(), "remote", "clef binary") 900 ctx = context.WithValue(ctx, "scheme", "in-proc") 901 ctx = context.WithValue(ctx, "local", "main") 902 errs := make([]string, 0) 903 904 a := common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") 905 addErr := func(errStr string) { 906 log.Info("Test error", "err", errStr) 907 errs = append(errs, errStr) 908 } 909 910 queryUser := func(q string) string { 911 resp, err := api.UI.OnInputRequired(core.UserInputRequest{ 912 Title: "Testing", 913 Prompt: q, 914 }) 915 if err != nil { 916 addErr(err.Error()) 917 } 918 return resp.Text 919 } 920 expectResponse := func(testcase, question, expect string) { 921 if got := queryUser(question); got != expect { 922 addErr(fmt.Sprintf("%s: got %v, expected %v", testcase, got, expect)) 923 } 924 } 925 expectApprove := func(testcase string, err error) { 926 if err == nil || err == accounts.ErrUnknownAccount { 927 return 928 } 929 addErr(fmt.Sprintf("%v: expected no error, got %v", testcase, err.Error())) 930 } 931 expectDeny := func(testcase string, err error) { 932 if err == nil || err != core.ErrRequestDenied { 933 addErr(fmt.Sprintf("%v: expected ErrRequestDenied, got %v", testcase, err)) 934 } 935 } 936 var delay = 1 * time.Second 937 // Test display of info and error 938 { 939 api.UI.ShowInfo("If you see this message, enter 'yes' to next question") 940 time.Sleep(delay) 941 expectResponse("showinfo", "Did you see the message? [yes/no]", "yes") 942 api.UI.ShowError("If you see this message, enter 'yes' to the next question") 943 time.Sleep(delay) 944 expectResponse("showerror", "Did you see the message? [yes/no]", "yes") 945 } 946 { // Sign data test - clique header 947 api.UI.ShowInfo("Please approve the next request for signing a clique header") 948 time.Sleep(delay) 949 cliqueHeader := types.Header{ 950 ParentHash: common.HexToHash("0000H45H"), 951 UncleHash: common.HexToHash("0000H45H"), 952 Coinbase: common.HexToAddress("0000H45H"), 953 Root: common.HexToHash("0000H00H"), 954 TxHash: common.HexToHash("0000H45H"), 955 ReceiptHash: common.HexToHash("0000H45H"), 956 Difficulty: big.NewInt(1337), 957 Number: big.NewInt(1337), 958 GasLimit: 1338, 959 GasUsed: 1338, 960 Time: 1338, 961 Extra: []byte("Extra data Extra data Extra data Extra data Extra data Extra data Extra data Extra data"), 962 MixDigest: common.HexToHash("0x0000H45H"), 963 } 964 cliqueRlp, err := rlp.EncodeToBytes(cliqueHeader) 965 if err != nil { 966 utils.Fatalf("Should not error: %v", err) 967 } 968 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 969 _, err = api.SignData(ctx, accounts.MimetypeClique, *addr, hexutil.Encode(cliqueRlp)) 970 expectApprove("signdata - clique header", err) 971 } 972 { // Sign data test - typed data 973 api.UI.ShowInfo("Please approve the next request for signing EIP-712 typed data") 974 time.Sleep(delay) 975 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 976 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!"}}` 977 //_, err := api.SignData(ctx, accounts.MimetypeTypedData, *addr, hexutil.Encode([]byte(data))) 978 var typedData apitypes.TypedData 979 json.Unmarshal([]byte(data), &typedData) 980 _, err := api.SignTypedData(ctx, *addr, typedData) 981 expectApprove("sign 712 typed data", err) 982 } 983 { // Sign data test - plain text 984 api.UI.ShowInfo("Please approve the next request for signing text") 985 time.Sleep(delay) 986 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 987 _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world"))) 988 expectApprove("signdata - text", err) 989 } 990 { // Sign data test - plain text reject 991 api.UI.ShowInfo("Please deny the next request for signing text") 992 time.Sleep(delay) 993 addr, _ := common.NewMixedcaseAddressFromString("0x0011223344556677889900112233445566778899") 994 _, err := api.SignData(ctx, accounts.MimetypeTextPlain, *addr, hexutil.Encode([]byte("hello world"))) 995 expectDeny("signdata - text", err) 996 } 997 { // Sign transaction 998 api.UI.ShowInfo("Please reject next transaction") 999 time.Sleep(delay) 1000 data := hexutil.Bytes([]byte{}) 1001 to := common.NewMixedcaseAddress(a) 1002 tx := apitypes.SendTxArgs{ 1003 Data: &data, 1004 Nonce: 0x1, 1005 Value: hexutil.Big(*big.NewInt(6)), 1006 From: common.NewMixedcaseAddress(a), 1007 To: &to, 1008 GasPrice: (*hexutil.Big)(big.NewInt(5)), 1009 Gas: 1000, 1010 Input: nil, 1011 } 1012 _, err := api.SignTransaction(ctx, tx, nil) 1013 expectDeny("signtransaction [1]", err) 1014 expectResponse("signtransaction [2]", "Did you see any warnings for the last transaction? (yes/no)", "no") 1015 } 1016 { // Listing 1017 api.UI.ShowInfo("Please reject listing-request") 1018 time.Sleep(delay) 1019 _, err := api.List(ctx) 1020 expectDeny("list", err) 1021 } 1022 { // Import 1023 api.UI.ShowInfo("Please reject new account-request") 1024 time.Sleep(delay) 1025 _, err := api.New(ctx) 1026 expectDeny("newaccount", err) 1027 } 1028 { // Metadata 1029 api.UI.ShowInfo("Please check if you see the Origin in next listing (approve or deny)") 1030 time.Sleep(delay) 1031 api.List(context.WithValue(ctx, "Origin", "origin.com")) 1032 expectResponse("metadata - origin", "Did you see origin (origin.com)? [yes/no] ", "yes") 1033 } 1034 1035 for _, e := range errs { 1036 log.Error(e) 1037 } 1038 result := fmt.Sprintf("Tests completed. %d errors:\n%s\n", len(errs), strings.Join(errs, "\n")) 1039 api.UI.ShowInfo(result) 1040 } 1041 1042 type encryptedSeedStorage struct { 1043 Description string `json:"description"` 1044 Version int `json:"version"` 1045 Params keystore.CryptoJSON `json:"params"` 1046 } 1047 1048 // encryptSeed uses a similar scheme as the keystore uses, but with a different wrapping, 1049 // to encrypt the master seed 1050 func encryptSeed(seed []byte, auth []byte, scryptN, scryptP int) ([]byte, error) { 1051 cryptoStruct, err := keystore.EncryptDataV3(seed, auth, scryptN, scryptP) 1052 if err != nil { 1053 return nil, err 1054 } 1055 return json.Marshal(&encryptedSeedStorage{"Clef seed", 1, cryptoStruct}) 1056 } 1057 1058 // decryptSeed decrypts the master seed 1059 func decryptSeed(keyjson []byte, auth string) ([]byte, error) { 1060 var encSeed encryptedSeedStorage 1061 if err := json.Unmarshal(keyjson, &encSeed); err != nil { 1062 return nil, err 1063 } 1064 if encSeed.Version != 1 { 1065 log.Warn(fmt.Sprintf("unsupported encryption format of seed: %d, operation will likely fail", encSeed.Version)) 1066 } 1067 seed, err := keystore.DecryptDataV3(encSeed.Params, auth) 1068 if err != nil { 1069 return nil, err 1070 } 1071 return seed, err 1072 } 1073 1074 // GenDoc outputs examples of all structures used in json-rpc communication 1075 func GenDoc(ctx *cli.Context) error { 1076 var ( 1077 a = common.HexToAddress("0xdeadbeef000000000000000000000000deadbeef") 1078 b = common.HexToAddress("0x1111111122222222222233333333334444444444") 1079 meta = core.Metadata{ 1080 Scheme: "http", 1081 Local: "localhost:8545", 1082 Origin: "www.malicious.ru", 1083 Remote: "localhost:9999", 1084 UserAgent: "Firefox 3.2", 1085 } 1086 output []string 1087 add = func(name, desc string, v interface{}) { 1088 if data, err := json.MarshalIndent(v, "", " "); err == nil { 1089 output = append(output, fmt.Sprintf("### %s\n\n%s\n\nExample:\n```json\n%s\n```", name, desc, data)) 1090 } else { 1091 log.Error("Error generating output", "err", err) 1092 } 1093 } 1094 ) 1095 1096 { // Sign plain text request 1097 desc := "SignDataRequest contains information about a pending request to sign some data. " + 1098 "The data to be signed can be of various types, defined by content-type. Clef has done most " + 1099 "of the work in canonicalizing and making sense of the data, and it's up to the UI to present" + 1100 "the user with the contents of the `message`" 1101 sighash, msg := accounts.TextAndHash([]byte("hello world")) 1102 messages := []*apitypes.NameValueType{{Name: "message", Value: msg, Typ: accounts.MimetypeTextPlain}} 1103 1104 add("SignDataRequest", desc, &core.SignDataRequest{ 1105 Address: common.NewMixedcaseAddress(a), 1106 Meta: meta, 1107 ContentType: accounts.MimetypeTextPlain, 1108 Rawdata: []byte(msg), 1109 Messages: messages, 1110 Hash: sighash}) 1111 } 1112 { // Sign plain text response 1113 add("SignDataResponse - approve", "Response to SignDataRequest", 1114 &core.SignDataResponse{Approved: true}) 1115 add("SignDataResponse - deny", "Response to SignDataRequest", 1116 &core.SignDataResponse{}) 1117 } 1118 { // Sign transaction request 1119 desc := "SignTxRequest contains information about a pending request to sign a transaction. " + 1120 "Aside from the transaction itself, there is also a `call_info`-struct. That struct contains " + 1121 "messages of various types, that the user should be informed of." + 1122 "\n\n" + 1123 "As in any request, it's important to consider that the `meta` info also contains untrusted data." + 1124 "\n\n" + 1125 "The `transaction` (on input into clef) can have either `data` or `input` -- if both are set, " + 1126 "they must be identical, otherwise an error is generated. " + 1127 "However, Clef will always use `data` when passing this struct on (if Clef does otherwise, please file a ticket)" 1128 1129 data := hexutil.Bytes([]byte{0x01, 0x02, 0x03, 0x04}) 1130 add("SignTxRequest", desc, &core.SignTxRequest{ 1131 Meta: meta, 1132 Callinfo: []apitypes.ValidationInfo{ 1133 {Typ: "Warning", Message: "Something looks odd, show this message as a warning"}, 1134 {Typ: "Info", Message: "User should see this as well"}, 1135 }, 1136 Transaction: apitypes.SendTxArgs{ 1137 Data: &data, 1138 Nonce: 0x1, 1139 Value: hexutil.Big(*big.NewInt(6)), 1140 From: common.NewMixedcaseAddress(a), 1141 To: nil, 1142 GasPrice: (*hexutil.Big)(big.NewInt(5)), 1143 Gas: 1000, 1144 Input: nil, 1145 }}) 1146 } 1147 { // Sign tx response 1148 data := hexutil.Bytes([]byte{0x04, 0x03, 0x02, 0x01}) 1149 add("SignTxResponse - approve", "Response to request to sign a transaction. This response needs to contain the `transaction`"+ 1150 ", because the UI is free to make modifications to the transaction.", 1151 &core.SignTxResponse{Approved: true, 1152 Transaction: apitypes.SendTxArgs{ 1153 Data: &data, 1154 Nonce: 0x4, 1155 Value: hexutil.Big(*big.NewInt(6)), 1156 From: common.NewMixedcaseAddress(a), 1157 To: nil, 1158 GasPrice: (*hexutil.Big)(big.NewInt(5)), 1159 Gas: 1000, 1160 Input: nil, 1161 }}) 1162 add("SignTxResponse - deny", "Response to SignTxRequest. When denying a request, there's no need to "+ 1163 "provide the transaction in return", 1164 &core.SignTxResponse{}) 1165 } 1166 { // WHen a signed tx is ready to go out 1167 desc := "SignTransactionResult is used in the call `clef` -> `OnApprovedTx(result)`" + 1168 "\n\n" + 1169 "This occurs _after_ successful completion of the entire signing procedure, but right before the signed " + 1170 "transaction is passed to the external caller. This method (and data) can be used by the UI to signal " + 1171 "to the user that the transaction was signed, but it is primarily useful for ruleset implementations." + 1172 "\n\n" + 1173 "A ruleset that implements a rate limitation needs to know what transactions are sent out to the external " + 1174 "interface. By hooking into this methods, the ruleset can maintain track of that count." + 1175 "\n\n" + 1176 "**OBS:** Note that if an attacker can restore your `clef` data to a previous point in time" + 1177 " (e.g through a backup), the attacker can reset such windows, even if he/she is unable to decrypt the content. " + 1178 "\n\n" + 1179 "The `OnApproved` method cannot be responded to, it's purely informative" 1180 1181 rlpdata := common.FromHex("0xf85d640101948a8eafb1cf62bfbeb1741769dae1a9dd47996192018026a0716bd90515acb1e68e5ac5867aa11a1e65399c3349d479f5fb698554ebc6f293a04e8a4ebfff434e971e0ef12c5bf3a881b06fd04fc3f8b8a7291fb67a26a1d4ed") 1182 var tx types.Transaction 1183 tx.UnmarshalBinary(rlpdata) 1184 add("OnApproved - SignTransactionResult", desc, ðapi.SignTransactionResult{Raw: rlpdata, Tx: &tx}) 1185 } 1186 { // User input 1187 add("UserInputRequest", "Sent when clef needs the user to provide data. If 'password' is true, the input field should be treated accordingly (echo-free)", 1188 &core.UserInputRequest{IsPassword: true, Title: "The title here", Prompt: "The question to ask the user"}) 1189 add("UserInputResponse", "Response to UserInputRequest", 1190 &core.UserInputResponse{Text: "The textual response from user"}) 1191 } 1192 { // List request 1193 add("ListRequest", "Sent when a request has been made to list addresses. The UI is provided with the "+ 1194 "full `account`s, including local directory names. Note: this information is not passed back to the external caller, "+ 1195 "who only sees the `address`es. ", 1196 &core.ListRequest{ 1197 Meta: meta, 1198 Accounts: []accounts.Account{ 1199 {Address: a, URL: accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/a"}}, 1200 {Address: b, URL: accounts.URL{Scheme: "keystore", Path: "/path/to/keyfile/b"}}}, 1201 }) 1202 1203 add("ListResponse", "Response to list request. The response contains a list of all addresses to show to the caller. "+ 1204 "Note: the UI is free to respond with any address the caller, regardless of whether it exists or not", 1205 &core.ListResponse{ 1206 Accounts: []accounts.Account{ 1207 { 1208 Address: common.HexToAddress("0xcowbeef000000cowbeef00000000000000000c0w"), 1209 URL: accounts.URL{Path: ".. ignored .."}, 1210 }, 1211 { 1212 Address: common.MaxAddress, 1213 }, 1214 }}) 1215 } 1216 1217 fmt.Println(`## UI Client interface 1218 1219 These data types are defined in the channel between clef and the UI`) 1220 for _, elem := range output { 1221 fmt.Println(elem) 1222 } 1223 return nil 1224 }