github.com/niluplatform/go-nilu@v1.7.4-0.20200912082737-a0cb0776d52c/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  // signer is a utility that can be used so sign transactions and
    18  // arbitrary data.
    19  package main
    20  
    21  import (
    22  	"bufio"
    23  	"context"
    24  	"crypto/rand"
    25  	"crypto/sha256"
    26  	"encoding/hex"
    27  	"encoding/json"
    28  	"fmt"
    29  	"io"
    30  	"io/ioutil"
    31  	"os"
    32  	"os/signal"
    33  	"os/user"
    34  	"path/filepath"
    35  	"runtime"
    36  	"strings"
    37  
    38  	"github.com/NiluPlatform/go-nilu/cmd/utils"
    39  	"github.com/NiluPlatform/go-nilu/common"
    40  	"github.com/NiluPlatform/go-nilu/crypto"
    41  	"github.com/NiluPlatform/go-nilu/log"
    42  	"github.com/NiluPlatform/go-nilu/node"
    43  	"github.com/NiluPlatform/go-nilu/rpc"
    44  	"github.com/NiluPlatform/go-nilu/signer/core"
    45  	"github.com/NiluPlatform/go-nilu/signer/rules"
    46  	"github.com/NiluPlatform/go-nilu/signer/storage"
    47  	"gopkg.in/urfave/cli.v1"
    48  )
    49  
    50  // ExternalAPIVersion -- see extapi_changelog.md
    51  const ExternalAPIVersion = "2.0.0"
    52  
    53  // InternalAPIVersion -- see intapi_changelog.md
    54  const InternalAPIVersion = "2.0.0"
    55  
    56  const legalWarning = `
    57  WARNING! 
    58  
    59  Clef is alpha software, and not yet publically released. This software has _not_ been audited, and there
    60  are no guarantees about the workings of this software. It may contain severe flaws. You should not use this software
    61  unless you agree to take full responsibility for doing so, and know what you are doing. 
    62  
    63  TLDR; THIS IS NOT PRODUCTION-READY SOFTWARE! 
    64  
    65  `
    66  
    67  var (
    68  	logLevelFlag = cli.IntFlag{
    69  		Name:  "loglevel",
    70  		Value: 4,
    71  		Usage: "log level to emit to the screen",
    72  	}
    73  	keystoreFlag = cli.StringFlag{
    74  		Name:  "keystore",
    75  		Value: filepath.Join(node.DefaultDataDir(), "keystore"),
    76  		Usage: "Directory for the keystore",
    77  	}
    78  	configdirFlag = cli.StringFlag{
    79  		Name:  "configdir",
    80  		Value: DefaultConfigDir(),
    81  		Usage: "Directory for Clef configuration",
    82  	}
    83  	rpcPortFlag = cli.IntFlag{
    84  		Name:  "rpcport",
    85  		Usage: "HTTP-RPC server listening port",
    86  		Value: node.DefaultHTTPPort + 5,
    87  	}
    88  	signerSecretFlag = cli.StringFlag{
    89  		Name:  "signersecret",
    90  		Usage: "A file containing the password used to encrypt Clef credentials, e.g. keystore credentials and ruleset hash",
    91  	}
    92  	dBFlag = cli.StringFlag{
    93  		Name:  "4bytedb",
    94  		Usage: "File containing 4byte-identifiers",
    95  		Value: "./4byte.json",
    96  	}
    97  	customDBFlag = cli.StringFlag{
    98  		Name:  "4bytedb-custom",
    99  		Usage: "File used for writing new 4byte-identifiers submitted via API",
   100  		Value: "./4byte-custom.json",
   101  	}
   102  	auditLogFlag = cli.StringFlag{
   103  		Name:  "auditlog",
   104  		Usage: "File used to emit audit logs. Set to \"\" to disable",
   105  		Value: "audit.log",
   106  	}
   107  	ruleFlag = cli.StringFlag{
   108  		Name:  "rules",
   109  		Usage: "Enable rule-engine",
   110  		Value: "rules.json",
   111  	}
   112  	stdiouiFlag = cli.BoolFlag{
   113  		Name: "stdio-ui",
   114  		Usage: "Use STDIN/STDOUT as a channel for an external UI. " +
   115  			"This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " +
   116  			"interface, and can be used when Clef is started by an external process.",
   117  	}
   118  	testFlag = cli.BoolFlag{
   119  		Name:  "stdio-ui-test",
   120  		Usage: "Mechanism to test interface between Clef and UI. Requires 'stdio-ui'.",
   121  	}
   122  	app         = cli.NewApp()
   123  	initCommand = cli.Command{
   124  		Action:    utils.MigrateFlags(initializeSecrets),
   125  		Name:      "init",
   126  		Usage:     "Initialize the signer, generate secret storage",
   127  		ArgsUsage: "",
   128  		Flags: []cli.Flag{
   129  			logLevelFlag,
   130  			configdirFlag,
   131  		},
   132  		Description: `
   133  The init command generates a master seed which Clef can use to store credentials and data needed for 
   134  the rule-engine to work.`,
   135  	}
   136  	attestCommand = cli.Command{
   137  		Action:    utils.MigrateFlags(attestFile),
   138  		Name:      "attest",
   139  		Usage:     "Attest that a js-file is to be used",
   140  		ArgsUsage: "<sha256sum>",
   141  		Flags: []cli.Flag{
   142  			logLevelFlag,
   143  			configdirFlag,
   144  			signerSecretFlag,
   145  		},
   146  		Description: `
   147  The attest command stores the sha256 of the rule.js-file that you want to use for automatic processing of 
   148  incoming requests. 
   149  
   150  Whenever you make an edit to the rule file, you need to use attestation to tell 
   151  Clef that the file is 'safe' to execute.`,
   152  	}
   153  
   154  	addCredentialCommand = cli.Command{
   155  		Action:    utils.MigrateFlags(addCredential),
   156  		Name:      "addpw",
   157  		Usage:     "Store a credential for a keystore file",
   158  		ArgsUsage: "<address> <password>",
   159  		Flags: []cli.Flag{
   160  			logLevelFlag,
   161  			configdirFlag,
   162  			signerSecretFlag,
   163  		},
   164  		Description: `
   165  The addpw command stores a password for a given address (keyfile). If you invoke it with only one parameter, it will 
   166  remove any stored credential for that address (keyfile)
   167  `,
   168  	}
   169  )
   170  
   171  func init() {
   172  	app.Name = "Clef"
   173  	app.Usage = "Manage Ethereum account operations"
   174  	app.Flags = []cli.Flag{
   175  		logLevelFlag,
   176  		keystoreFlag,
   177  		configdirFlag,
   178  		utils.NetworkIdFlag,
   179  		utils.LightKDFFlag,
   180  		utils.NoUSBFlag,
   181  		utils.RPCListenAddrFlag,
   182  		utils.RPCVirtualHostsFlag,
   183  		utils.IPCDisabledFlag,
   184  		utils.IPCPathFlag,
   185  		utils.RPCEnabledFlag,
   186  		rpcPortFlag,
   187  		signerSecretFlag,
   188  		dBFlag,
   189  		customDBFlag,
   190  		auditLogFlag,
   191  		ruleFlag,
   192  		stdiouiFlag,
   193  		testFlag,
   194  	}
   195  	app.Action = signer
   196  	app.Commands = []cli.Command{initCommand, attestCommand, addCredentialCommand}
   197  
   198  }
   199  func main() {
   200  	if err := app.Run(os.Args); err != nil {
   201  		fmt.Fprintln(os.Stderr, err)
   202  		os.Exit(1)
   203  	}
   204  }
   205  
   206  func initializeSecrets(c *cli.Context) error {
   207  	if err := initialize(c); err != nil {
   208  		return err
   209  	}
   210  	configDir := c.String(configdirFlag.Name)
   211  
   212  	masterSeed := make([]byte, 256)
   213  	n, err := io.ReadFull(rand.Reader, masterSeed)
   214  	if err != nil {
   215  		return err
   216  	}
   217  	if n != len(masterSeed) {
   218  		return fmt.Errorf("failed to read enough random")
   219  	}
   220  	err = os.Mkdir(configDir, 0700)
   221  	if err != nil && !os.IsExist(err) {
   222  		return err
   223  	}
   224  	location := filepath.Join(configDir, "secrets.dat")
   225  	if _, err := os.Stat(location); err == nil {
   226  		return fmt.Errorf("file %v already exists, will not overwrite", location)
   227  	}
   228  	err = ioutil.WriteFile(location, masterSeed, 0700)
   229  	if err != nil {
   230  		return err
   231  	}
   232  	fmt.Printf("A master seed has been generated into %s\n", location)
   233  	fmt.Printf(`
   234  This is required to be able to store credentials, such as : 
   235  * Passwords for keystores (used by rule engine)
   236  * Storage for javascript rules
   237  * Hash of rule-file
   238  
   239  You should treat that file with utmost secrecy, and make a backup of it. 
   240  NOTE: This file does not contain your accounts. Those need to be backed up separately!
   241  
   242  `)
   243  	return nil
   244  }
   245  func attestFile(ctx *cli.Context) error {
   246  	if len(ctx.Args()) < 1 {
   247  		utils.Fatalf("This command requires an argument.")
   248  	}
   249  	if err := initialize(ctx); err != nil {
   250  		return err
   251  	}
   252  
   253  	stretchedKey, err := readMasterKey(ctx)
   254  	if err != nil {
   255  		utils.Fatalf(err.Error())
   256  	}
   257  	configDir := ctx.String(configdirFlag.Name)
   258  	vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
   259  	confKey := crypto.Keccak256([]byte("config"), stretchedKey)
   260  
   261  	// Initialize the encrypted storages
   262  	configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confKey)
   263  	val := ctx.Args().First()
   264  	configStorage.Put("ruleset_sha256", val)
   265  	log.Info("Ruleset attestation updated", "sha256", val)
   266  	return nil
   267  }
   268  
   269  func addCredential(ctx *cli.Context) error {
   270  	if len(ctx.Args()) < 1 {
   271  		utils.Fatalf("This command requires at leaste one argument.")
   272  	}
   273  	if err := initialize(ctx); err != nil {
   274  		return err
   275  	}
   276  
   277  	stretchedKey, err := readMasterKey(ctx)
   278  	if err != nil {
   279  		utils.Fatalf(err.Error())
   280  	}
   281  	configDir := ctx.String(configdirFlag.Name)
   282  	vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
   283  	pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
   284  
   285  	// Initialize the encrypted storages
   286  	pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
   287  	key := ctx.Args().First()
   288  	value := ""
   289  	if len(ctx.Args()) > 1 {
   290  		value = ctx.Args().Get(1)
   291  	}
   292  	pwStorage.Put(key, value)
   293  	log.Info("Credential store updated", "key", key)
   294  	return nil
   295  }
   296  
   297  func initialize(c *cli.Context) error {
   298  	// Set up the logger to print everything
   299  	logOutput := os.Stdout
   300  	if c.Bool(stdiouiFlag.Name) {
   301  		logOutput = os.Stderr
   302  		// If using the stdioui, we can't do the 'confirm'-flow
   303  		fmt.Fprintf(logOutput, legalWarning)
   304  	} else {
   305  		if !confirm(legalWarning) {
   306  			return fmt.Errorf("aborted by user")
   307  		}
   308  	}
   309  
   310  	log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int(logLevelFlag.Name)), log.StreamHandler(logOutput, log.TerminalFormat(true))))
   311  	return nil
   312  }
   313  
   314  func signer(c *cli.Context) error {
   315  	if err := initialize(c); err != nil {
   316  		return err
   317  	}
   318  	var (
   319  		ui core.SignerUI
   320  	)
   321  	if c.Bool(stdiouiFlag.Name) {
   322  		log.Info("Using stdin/stdout as UI-channel")
   323  		ui = core.NewStdIOUI()
   324  	} else {
   325  		log.Info("Using CLI as UI-channel")
   326  		ui = core.NewCommandlineUI()
   327  	}
   328  	db, err := core.NewAbiDBFromFiles(c.String(dBFlag.Name), c.String(customDBFlag.Name))
   329  	if err != nil {
   330  		utils.Fatalf(err.Error())
   331  	}
   332  	log.Info("Loaded 4byte db", "signatures", db.Size(), "file", c.String("4bytedb"))
   333  
   334  	var (
   335  		api core.ExternalAPI
   336  	)
   337  
   338  	configDir := c.String(configdirFlag.Name)
   339  	if stretchedKey, err := readMasterKey(c); err != nil {
   340  		log.Info("No master seed provided, rules disabled")
   341  	} else {
   342  
   343  		if err != nil {
   344  			utils.Fatalf(err.Error())
   345  		}
   346  		vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), stretchedKey)[:10]))
   347  
   348  		// Generate domain specific keys
   349  		pwkey := crypto.Keccak256([]byte("credentials"), stretchedKey)
   350  		jskey := crypto.Keccak256([]byte("jsstorage"), stretchedKey)
   351  		confkey := crypto.Keccak256([]byte("config"), stretchedKey)
   352  
   353  		// Initialize the encrypted storages
   354  		pwStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "credentials.json"), pwkey)
   355  		jsStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "jsstorage.json"), jskey)
   356  		configStorage := storage.NewAESEncryptedStorage(filepath.Join(vaultLocation, "config.json"), confkey)
   357  
   358  		//Do we have a rule-file?
   359  		ruleJS, err := ioutil.ReadFile(c.String(ruleFlag.Name))
   360  		if err != nil {
   361  			log.Info("Could not load rulefile, rules not enabled", "file", "rulefile")
   362  		} else {
   363  			hasher := sha256.New()
   364  			hasher.Write(ruleJS)
   365  			shasum := hasher.Sum(nil)
   366  			storedShasum := configStorage.Get("ruleset_sha256")
   367  			if storedShasum != hex.EncodeToString(shasum) {
   368  				log.Info("Could not validate ruleset hash, rules not enabled", "got", hex.EncodeToString(shasum), "expected", storedShasum)
   369  			} else {
   370  				// Initialize rules
   371  				ruleEngine, err := rules.NewRuleEvaluator(ui, jsStorage, pwStorage)
   372  				if err != nil {
   373  					utils.Fatalf(err.Error())
   374  				}
   375  				ruleEngine.Init(string(ruleJS))
   376  				ui = ruleEngine
   377  				log.Info("Rule engine configured", "file", c.String(ruleFlag.Name))
   378  			}
   379  		}
   380  	}
   381  
   382  	apiImpl := core.NewSignerAPI(
   383  		c.Int64(utils.NetworkIdFlag.Name),
   384  		c.String(keystoreFlag.Name),
   385  		c.Bool(utils.NoUSBFlag.Name),
   386  		ui, db,
   387  		c.Bool(utils.LightKDFFlag.Name))
   388  
   389  	api = apiImpl
   390  
   391  	// Audit logging
   392  	if logfile := c.String(auditLogFlag.Name); logfile != "" {
   393  		api, err = core.NewAuditLogger(logfile, api)
   394  		if err != nil {
   395  			utils.Fatalf(err.Error())
   396  		}
   397  		log.Info("Audit logs configured", "file", logfile)
   398  	}
   399  	// register signer API with server
   400  	var (
   401  		extapiURL = "n/a"
   402  		ipcapiURL = "n/a"
   403  	)
   404  	rpcAPI := []rpc.API{
   405  		{
   406  			Namespace: "account",
   407  			Public:    true,
   408  			Service:   api,
   409  			Version:   "1.0"},
   410  	}
   411  	if c.Bool(utils.RPCEnabledFlag.Name) {
   412  
   413  		vhosts := splitAndTrim(c.GlobalString(utils.RPCVirtualHostsFlag.Name))
   414  		cors := splitAndTrim(c.GlobalString(utils.RPCCORSDomainFlag.Name))
   415  
   416  		// start http server
   417  		httpEndpoint := fmt.Sprintf("%s:%d", c.String(utils.RPCListenAddrFlag.Name), c.Int(rpcPortFlag.Name))
   418  		listener, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcAPI, []string{"account"}, cors, vhosts)
   419  		if err != nil {
   420  			utils.Fatalf("Could not start RPC api: %v", err)
   421  		}
   422  		extapiURL = fmt.Sprintf("http://%s", httpEndpoint)
   423  		log.Info("HTTP endpoint opened", "url", extapiURL)
   424  
   425  		defer func() {
   426  			listener.Close()
   427  			log.Info("HTTP endpoint closed", "url", httpEndpoint)
   428  		}()
   429  
   430  	}
   431  	if !c.Bool(utils.IPCDisabledFlag.Name) {
   432  		if c.IsSet(utils.IPCPathFlag.Name) {
   433  			ipcapiURL = c.String(utils.IPCPathFlag.Name)
   434  		} else {
   435  			ipcapiURL = filepath.Join(configDir, "clef.ipc")
   436  		}
   437  
   438  		listener, _, err := rpc.StartIPCEndpoint(ipcapiURL, rpcAPI)
   439  		if err != nil {
   440  			utils.Fatalf("Could not start IPC api: %v", err)
   441  		}
   442  		log.Info("IPC endpoint opened", "url", ipcapiURL)
   443  		defer func() {
   444  			listener.Close()
   445  			log.Info("IPC endpoint closed", "url", ipcapiURL)
   446  		}()
   447  
   448  	}
   449  
   450  	if c.Bool(testFlag.Name) {
   451  		log.Info("Performing UI test")
   452  		go testExternalUI(apiImpl)
   453  	}
   454  	ui.OnSignerStartup(core.StartupInfo{
   455  		Info: map[string]interface{}{
   456  			"extapi_version": ExternalAPIVersion,
   457  			"intapi_version": InternalAPIVersion,
   458  			"extapi_http":    extapiURL,
   459  			"extapi_ipc":     ipcapiURL,
   460  		},
   461  	})
   462  
   463  	abortChan := make(chan os.Signal)
   464  	signal.Notify(abortChan, os.Interrupt)
   465  
   466  	sig := <-abortChan
   467  	log.Info("Exiting...", "signal", sig)
   468  
   469  	return nil
   470  }
   471  
   472  // splitAndTrim splits input separated by a comma
   473  // and trims excessive white space from the substrings.
   474  func splitAndTrim(input string) []string {
   475  	result := strings.Split(input, ",")
   476  	for i, r := range result {
   477  		result[i] = strings.TrimSpace(r)
   478  	}
   479  	return result
   480  }
   481  
   482  // DefaultConfigDir is the default config directory to use for the vaults and other
   483  // persistence requirements.
   484  func DefaultConfigDir() string {
   485  	// Try to place the data folder in the user's home dir
   486  	home := homeDir()
   487  	if home != "" {
   488  		if runtime.GOOS == "darwin" {
   489  			return filepath.Join(home, "Library", "Signer")
   490  		} else if runtime.GOOS == "windows" {
   491  			return filepath.Join(home, "AppData", "Roaming", "Signer")
   492  		} else {
   493  			return filepath.Join(home, ".clef")
   494  		}
   495  	}
   496  	// As we cannot guess a stable location, return empty and handle later
   497  	return ""
   498  }
   499  
   500  func homeDir() string {
   501  	if home := os.Getenv("HOME"); home != "" {
   502  		return home
   503  	}
   504  	if usr, err := user.Current(); err == nil {
   505  		return usr.HomeDir
   506  	}
   507  	return ""
   508  }
   509  func readMasterKey(ctx *cli.Context) ([]byte, error) {
   510  	var (
   511  		file      string
   512  		configDir = ctx.String(configdirFlag.Name)
   513  	)
   514  	if ctx.IsSet(signerSecretFlag.Name) {
   515  		file = ctx.String(signerSecretFlag.Name)
   516  	} else {
   517  		file = filepath.Join(configDir, "secrets.dat")
   518  	}
   519  	if err := checkFile(file); err != nil {
   520  		return nil, err
   521  	}
   522  	masterKey, err := ioutil.ReadFile(file)
   523  	if err != nil {
   524  		return nil, err
   525  	}
   526  	if len(masterKey) < 256 {
   527  		return nil, fmt.Errorf("master key of insufficient length, expected >255 bytes, got %d", len(masterKey))
   528  	}
   529  	// Create vault location
   530  	vaultLocation := filepath.Join(configDir, common.Bytes2Hex(crypto.Keccak256([]byte("vault"), masterKey)[:10]))
   531  	err = os.Mkdir(vaultLocation, 0700)
   532  	if err != nil && !os.IsExist(err) {
   533  		return nil, err
   534  	}
   535  	//!TODO, use KDF to stretch the master key
   536  	//			stretched_key := stretch_key(master_key)
   537  
   538  	return masterKey, nil
   539  }
   540  
   541  // checkFile is a convenience function to check if a file
   542  // * exists
   543  // * is mode 0600
   544  func checkFile(filename string) error {
   545  	info, err := os.Stat(filename)
   546  	if err != nil {
   547  		return fmt.Errorf("failed stat on %s: %v", filename, err)
   548  	}
   549  	// Check the unix permission bits
   550  	if info.Mode().Perm()&077 != 0 {
   551  		return fmt.Errorf("file (%v) has insecure file permissions (%v)", filename, info.Mode().String())
   552  	}
   553  	return nil
   554  }
   555  
   556  // confirm displays a text and asks for user confirmation
   557  func confirm(text string) bool {
   558  	fmt.Printf(text)
   559  	fmt.Printf("\nEnter 'ok' to proceed:\n>")
   560  
   561  	text, err := bufio.NewReader(os.Stdin).ReadString('\n')
   562  	if err != nil {
   563  		log.Crit("Failed to read user input", "err", err)
   564  	}
   565  
   566  	if text := strings.TrimSpace(text); text == "ok" {
   567  		return true
   568  	}
   569  	return false
   570  }
   571  
   572  func testExternalUI(api *core.SignerAPI) {
   573  
   574  	ctx := context.WithValue(context.Background(), "remote", "clef binary")
   575  	ctx = context.WithValue(ctx, "scheme", "in-proc")
   576  	ctx = context.WithValue(ctx, "local", "main")
   577  
   578  	errs := make([]string, 0)
   579  
   580  	api.UI.ShowInfo("Testing 'ShowInfo'")
   581  	api.UI.ShowError("Testing 'ShowError'")
   582  
   583  	checkErr := func(method string, err error) {
   584  		if err != nil && err != core.ErrRequestDenied {
   585  			errs = append(errs, fmt.Sprintf("%v: %v", method, err.Error()))
   586  		}
   587  	}
   588  	var err error
   589  
   590  	_, err = api.SignTransaction(ctx, core.SendTxArgs{From: common.MixedcaseAddress{}}, nil)
   591  	checkErr("SignTransaction", err)
   592  	_, err = api.Sign(ctx, common.MixedcaseAddress{}, common.Hex2Bytes("01020304"))
   593  	checkErr("Sign", err)
   594  	_, err = api.List(ctx)
   595  	checkErr("List", err)
   596  	_, err = api.New(ctx)
   597  	checkErr("New", err)
   598  	_, err = api.Export(ctx, common.Address{})
   599  	checkErr("Export", err)
   600  	_, err = api.Import(ctx, json.RawMessage{})
   601  	checkErr("Import", err)
   602  
   603  	api.UI.ShowInfo("Tests completed")
   604  
   605  	if len(errs) > 0 {
   606  		log.Error("Got errors")
   607  		for _, e := range errs {
   608  			log.Error(e)
   609  		}
   610  	} else {
   611  		log.Info("No errors")
   612  	}
   613  
   614  }
   615  
   616  /**
   617  //Create Account
   618  
   619  curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_new","params":["test"],"id":67}' localhost:8550
   620  
   621  // List accounts
   622  
   623  curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_list","params":[""],"id":67}' http://localhost:8550/
   624  
   625  // Make Transaction
   626  // safeSend(0x12)
   627  // 4401a6e40000000000000000000000000000000000000000000000000000000000000012
   628  
   629  // supplied abi
   630  curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x82A2A876D39022B3019932D30Cd9c97ad5616813","gas":"0x333","gasPrice":"0x123","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x10", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"},"test"],"id":67}' http://localhost:8550/
   631  
   632  // Not supplied
   633  curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_signTransaction","params":[{"from":"0x82A2A876D39022B3019932D30Cd9c97ad5616813","gas":"0x333","gasPrice":"0x123","nonce":"0x0","to":"0x07a565b7ed7d7a678680a4c162885bedbb695fe0", "value":"0x10", "data":"0x4401a6e40000000000000000000000000000000000000000000000000000000000000012"}],"id":67}' http://localhost:8550/
   634  
   635  // Sign data
   636  
   637  curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_sign","params":["0x694267f14675d7e1b9494fd8d72fefe1755710fa","bazonk gaz baz"],"id":67}' http://localhost:8550/
   638  
   639  
   640  **/