From 46fa90e9c8788053070aadf781a973624847f420 Mon Sep 17 00:00:00 2001
From: Benjamin Bollen <ben@erisindustries.com>
Date: Mon, 1 Aug 2016 18:02:04 +0200
Subject: [PATCH] client: sketch out command and core; carry over from
 mint-client/mintx

---
 client/cmd/eris-client.go         |  81 +++-
 client/cmd/transaction.go         | 142 ++++++-
 client/core/core.go               | 661 ++++++++++++++++++++++++++++++
 client/transaction/transaction.go |  35 ++
 cmd/eris-db.go                    |  12 +-
 definitions/client_do.go          |  78 ++++
 definitions/do.go                 |   2 +-
 7 files changed, 996 insertions(+), 15 deletions(-)
 create mode 100644 client/core/core.go
 create mode 100644 client/transaction/transaction.go
 create mode 100644 definitions/client_do.go

diff --git a/client/cmd/eris-client.go b/client/cmd/eris-client.go
index c7f67471..6df5e2a0 100644
--- a/client/cmd/eris-client.go
+++ b/client/cmd/eris-client.go
@@ -17,10 +17,20 @@
 package commands
 
 import (
+	"os"
+	"strconv"
+	"strings"
+
+	"github.com/spf13/cobra13"
+
+	"github.com/eris-ltd/eris-logger"
+
+	"github.com/eris-ltd/eris-db/definitions"
+	"github.com/eris-ltd/eris-db/version"
 )
 
-// Global Do struct
-var do *definitions.Do
+// Global flags for persistent flags
+var clientDo *definitions.ClientDo
 
 var ErisClientCmd = &cobra.Command{
 	Use:   "eris-client",
@@ -34,9 +44,9 @@ Complete documentation is available at https://docs.erisindustries.com
 	PersistentPreRun: func(cmd *cobra.Command, args []string) {
 
 		log.SetLevel(log.WarnLevel)
-		if do.Verbose {
+		if clientDo.Verbose {
 			log.SetLevel(log.InfoLevel)
-		} else if do.Debug {
+		} else if clientDo.Debug {
 			log.SetLevel(log.DebugLevel)
 		}
 	},
@@ -44,5 +54,66 @@ Complete documentation is available at https://docs.erisindustries.com
 }
 
 func Execute() {
+	InitErisClientInit()
+	AddGlobalFlags()
+	AddClientCommands()
+	ErisClientCmd.Execute()
+}
+
+func InitErisClientInit() {
+	// initialise an empty ClientDo struct for command execution
+	clientDo = definitions.NewClientDo()
+}
+
+func AddGlobalFlags() {
+	ErisClientCmd.PersistentFlags().BoolVarP(&clientDo.Verbose, "verbose", "v", defaultVerbose(), "verbose output; more output than no output flags; less output than debug level; default respects $ERIS_CLIENT_VERBOSE")
+	ErisClientCmd.PersistentFlags().BoolVarP(&clientDo.Debug, "debug", "d", defaultDebug(), "debug level output; the most output available for eris-client; if it is too chatty use verbose flag; default respects $ERIS_CLIENT_DEBUG")
+}
+
+func AddClientCommands() {
+	buildTransactionCommand()
+	ErisClientCmd.AddCommand(TransactionCmd)
+}
+
+//------------------------------------------------------------------------------
+// Defaults
 
-}
\ No newline at end of file
+// defaultVerbose is set to false unless the ERIS_CLIENT_VERBOSE environment
+// variable is set to a parsable boolean.
+func defaultVerbose() bool {
+	return setDefaultBool("ERIS_CLIENT_VERBOSE", false)
+}
+
+// defaultDebug is set to false unless the ERIS_CLIENT_DEBUG environment
+// variable is set to a parsable boolean.
+func defaultDebug() bool {
+	return setDefaultBool("ERIS_CLIENT_DEBUG", false)
+}
+
+// setDefaultBool returns the provided default value if the environment variable
+// is not set or not parsable as a bool.
+func setDefaultBool(environmentVariable string, defaultValue bool) bool {
+	value := os.Getenv(environmentVariable)
+	if value != "" {
+		if parsedValue, err := strconv.ParseBool(value); err == nil {
+			return parsedValue
+		}
+	}
+	return defaultValue
+}
+
+func setDefaultString(envVar, def string) string {
+	env := os.Getenv(envVar)
+	if env != "" {
+		return env
+	}
+	return def
+}
+
+func setDefaultStringSlice(envVar string, def []string) []string {
+	env := os.Getenv(envVar)
+	if env != "" {
+		return strings.Split(env, ",")
+	}
+	return def
+}
diff --git a/client/cmd/transaction.go b/client/cmd/transaction.go
index b328e129..70d56c9f 100644
--- a/client/cmd/transaction.go
+++ b/client/cmd/transaction.go
@@ -17,9 +17,145 @@
 package commands
 
 import (
-	commands "github.com/eris-ltd/eris-db/cmd"
+	"strings"
+	cobra "github.com/spf13/cobra"
 )
 
-func main() {
-	commands.Execute()
+var TransactionCmd = &cobra.Command(
+	Use:   "tx",
+	Short: "eris-client tx formulates and signs a transaction to a chain",
+	Long:  `eris-client tx formulates and signs a transaction to a chain`,
+	Run:   func(cmd *cobra.Command, args []string) { cmd.Help() },
+	PersistentPreRun: func(cmd *cobra.Command, args []string) {
+		// 
+		if !strings.HasPrefix(clientDo.nodeAddrFlag, "http://") {
+			clientDo.nodeAddrFlag = "http://" + clientDo.nodeAddrFlag
+		}
+		if !strings.HasSuffix(clientDo.nodeAddrFlag, "/") {
+			clientDo.nodeAddrFlag += "/"
+		}
+
+		if !strings.HasPrefix(clientDo.signAddrFlag, "http://") {
+			ClientDo.signAddrFlag = "http://" + clientDo.signAddrFlag
+		}
+	},
+)
+
+func buildTransactionCommand() {
+	// Transaction command has subcommands send, name, call, bond,
+	// unbond, rebond, permissions. Dupeout transaction is not accessible through the command line.
+
+	addTransactionPersistentFlags()
+
+	// SendTx
+	var sendCmd = &cobra.Command{
+		Use:   "send",
+		Short: "eris-client tx send --amt <amt> --to <addr>",
+		Long:  "eris-client tx send --amt <amt> --to <addr>",
+		Run:   cliSend,	
+	}
+	sendCmd.Flags().StringVarP(&clientDo.amtFlag, "amt", "a", "", "specify an amount")
+	sendCmd.Flags().StringVarP(&clientDo.toFlag, "to", "t", "", "specify an address to send to")
+
+	// NameTx
+	var nameCmd = &cobra.Command{
+		Use:   "name",
+		Short: "eris-client tx name --amt <amt> --name <name> --data <data>",
+		Long:  "eris-client tx name --amt <amt> --name <name> --data <data>",
+		Run:   cliName,
+	}
+	nameCmd.Flags().StringVarP(&clientDo.amtFlag, "amt", "a", "", "specify an amount")
+	nameCmd.Flags().StringVarP(&clientDo.nameFlag, "name", "n", "", "specify a name")
+	nameCmd.Flags().StringVarP(&clientDo.dataFlag, "data", "d", "", "specify some data")
+	nameCmd.Flags().StringVarP(&clientDo.dataFileFlag, "data-file", "", "", "specify a file with some data")
+	nameCmd.Flags().StringVarP(&clientDo.feeFlag, "fee", "f", "", "specify the fee to send")
+
+	// CallTx
+	var callCmd = &cobra.Command{
+		Use:   "call",
+		Short: "eris-client tx call --amt <amt> --fee <fee> --gas <gas> --to <contract addr> --data <data>",
+		Long:  "eris-client tx call --amt <amt> --fee <fee> --gas <gas> --to <contract addr> --data <data>",
+		Run:   cliCall,
+	}
+	callCmd.Flags().StringVarP(&clientDo.amtFlag, "amt", "a", "", "specify an amount")
+	callCmd.Flags().StringVarP(&clientDo.toFlag, "to", "t", "", "specify an address to send to")
+	callCmd.Flags().StringVarP(&clientDo.dataFlag, "data", "d", "", "specify some data")
+	callCmd.Flags().StringVarP(&clientDo.feeFlag, "fee", "f", "", "specify the fee to send")
+	callCmd.Flags().StringVarP(&clientDo.gasFlag, "gas", "g", "", "specify the gas limit for a CallTx")
+
+	// BondTx
+	var bondCmd = &cobra.Command{
+		Use:   "bond",
+		Short: "eris-client tx bond --pubkey <pubkey> --amt <amt> --unbond-to <address>",
+		Long:  "eris-client tx bond --pubkey <pubkey> --amt <amt> --unbond-to <address>",
+		Run:   cliBond,
+	}
+	bondCmd.Flags().StringVarP(&clientDo.amtFlag, "amt", "a", "", "specify an amount")
+	bondCmd.Flags().StringVarP(&clientDo.unbondtoFlag, "to", "t", "", "specify an address to unbond to")
+
+	// UnbondTx
+	var unbondCmd = &cobra.Command{
+		Use:   "unbond",
+		Short: "eris-client tx unbond --addr <address> --height <block_height>",
+		Long:  "eris-client tx unbond --addr <address> --height <block_height>",
+		Run:   cliUnbond,
+	}
+	unbondCmd.Flags().StringVarP(&clientDo.addrFlag, "addr", "a", "", "specify an address")
+	unbondCmd.Flags().StringVarP(&clientDo.heightFlag, "height", "n", "", "specify a height to unbond at")
+
+	// RebondTx
+	var rebondCmd = &cobra.Command{
+		Use:   "rebond",
+		Short: "eris-client tx rebond --addr <address> --height <block_height>",
+		Long:  "eris-client tx rebond --addr <address> --height <block_height>",
+		Run:   cliRebond,
+	}
+	rebondCmd.Flags().StringVarP(&clientDo.addrFlag, "addr", "a", "", "specify an address")
+	rebondCmd.Flags().StringVarP(&clientDo.heightFlag, "height", "n", "", "specify a height to unbond at")
+
+	// PermissionsTx
+	var permissionsCmd = &cobra.Command{
+		Use:   "permission",
+		Short: "eris-client tx perm <function name> <args ...>",
+		Long:  "eris-client tx perm <function name> <args ...>",
+		Run:   cliPermissions,
+	}
+	permissionsCmd.Flags().StringVarP(&clientDo.addrFlag, "addr", "a", "", "specify an address")
+	permissionsCmd.Flags().StringVarP(&clientDo.heightFlag, "height", "n", "", "specify a height to unbond at")
+}
+
+func addTransactionPersistentFlags() {
+	ErisClientCmd.PersistentFlags().StringVarP(&clientDo.signAddrFlag, "sign-addr", "", defaultKeyDaemonAddr, "set eris-keys daemon address (default respects $ERIS_CLIENT_SIGN_ADDRESS)")
+	ErisClientCmd.PersistentFlags().StringVarP(&clientDo.nodeAddrFlag, "node-addr", "", DefaultNodeRPCAddr, "set the eris-db node rpc server address (default respects $ERIS_CLIENT_NODE_ADDRESS)")
+	ErisClientCmd.PersistentFlags().StringVarP(&clientDo.pubkeyFlag, "pubkey", "", defaultPublicKey, "specify the public key to sign with (defaults to $ERIS_CLIENT_PUBLIC_KEY)")
+	ErisClientCmd.PersistentFlags().StringVarP(&clientDo.addrFlag, "addr", "", defaultAddress, "specify the account address (from which the public key can be fetch from eris-keys) (default respects $ERIS_CLIENT_ADDRESS)")
+	ErisClientCmd.PersistentFlags().StringVarP(&clientDo.chainidFlag, "chain-id", "", defaultChainID, "specify the chainID (default respects $CHAIN_ID)")
+	ErisClientCmd.PersistentFlags().StringVarP(&clientDo.nonceFlag, "nonce", "", "", "specify the nonce to use for the transaction (should equal the sender account's nonce + 1)")
+
+	// ErisClientCmd.PersistentFlags().BoolVarP(&signFlag, "sign", "s", false, "sign the transaction using the eris-keys daemon")
+	ErisClientCmd.PersistentFlags().BoolVarP(&clientDo.broadcastFlag, "broadcast", "b", false, "broadcast the transaction to the blockchain")
+	ErisClientCmd.PersistentFlags().BoolVarP(&clientDo.waitFlag, "wait", "w", false, "wait for the transaction to be committed in a block")
+}
+
+//------------------------------------------------------------------------------
+// Defaults
+
+func defaultChainId() string {
+	return setDefaultString("CHAIN_ID", "")
+}
+
+func defaultKeyDaemonAddress() string {
+	return setDefaultString("ERIS_CLIENT_SIGN_ADDRESS", "http://localhost:4767")
+}
+
+func defaultNodeRpcAddress() string {
+	return setDefaultString("ERIS_CLIENT_NODE_ADDRESS", "http://localhost:46657")
+}
+
+func defaultPublicKey() string {
+	return setDefaultString("ERIS_CLIENT_PUBLIC_KEY", "")
+}
+
+func defaultAddress() string {
+	return setDefaultString("ERIS_CLIENT_ADDRESS", "")
 }
diff --git a/client/core/core.go b/client/core/core.go
new file mode 100644
index 00000000..df6dc8c0
--- /dev/null
+++ b/client/core/core.go
@@ -0,0 +1,661 @@
+// Copyright 2015, 2016 Eris Industries (UK) Ltd.
+// This file is part of Eris-RT
+
+// Eris-RT is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Eris-RT is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Eris-RT.  If not, see <http://www.gnu.org/licenses/>.
+
+package core
+
+package core
+
+import (
+	"bytes"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/eris-ltd/tendermint/account"
+	ptypes "github.com/eris-ltd/tendermint/permission/types"
+	rtypes "github.com/eris-ltd/tendermint/rpc/core/types"
+	cclient "github.com/eris-ltd/tendermint/rpc/core_client"
+	"github.com/eris-ltd/txs"
+)
+
+var (
+	MaxCommitWaitTimeSeconds = 20
+)
+
+//------------------------------------------------------------------------------------
+// core functions with string args.
+// validates strings and forms transaction
+
+func Send(nodeAddr, signAddr, pubkey, addr, toAddr, amtS, nonceS string) (*txs.SendTx, error) {
+	pub, amt, nonce, err := checkCommon(nodeAddr, signAddr, pubkey, addr, amtS, nonceS)
+	if err != nil {
+		return nil, err
+	}
+
+	if toAddr == "" {
+		return nil, fmt.Errorf("destination address must be given with --to flag")
+	}
+
+	toAddrBytes, err := hex.DecodeString(toAddr)
+	if err != nil {
+		return nil, fmt.Errorf("toAddr is bad hex: %v", err)
+	}
+
+	tx := txs.NewSendTx()
+	tx.AddInputWithNonce(pub, amt, int(nonce))
+	tx.AddOutput(toAddrBytes, amt)
+
+	return tx, nil
+}
+
+func Call(nodeAddr, signAddr, pubkey, addr, toAddr, amtS, nonceS, gasS, feeS, data string) (*txs.CallTx, error) {
+	pub, amt, nonce, err := checkCommon(nodeAddr, signAddr, pubkey, addr, amtS, nonceS)
+	if err != nil {
+		return nil, err
+	}
+
+	toAddrBytes, err := hex.DecodeString(toAddr)
+	if err != nil {
+		return nil, fmt.Errorf("toAddr is bad hex: %v", err)
+	}
+
+	fee, err := strconv.ParseInt(feeS, 10, 64)
+	if err != nil {
+		return nil, fmt.Errorf("fee is misformatted: %v", err)
+	}
+
+	gas, err := strconv.ParseInt(gasS, 10, 64)
+	if err != nil {
+		return nil, fmt.Errorf("gas is misformatted: %v", err)
+	}
+
+	dataBytes, err := hex.DecodeString(data)
+	if err != nil {
+		return nil, fmt.Errorf("data is bad hex: %v", err)
+	}
+
+	tx := types.NewCallTxWithNonce(pub, toAddrBytes, dataBytes, amt, gas, fee, int(nonce))
+	return tx, nil
+}
+
+func Name(nodeAddr, signAddr, pubkey, addr, amtS, nonceS, feeS, name, data string) (*txs.NameTx, error) {
+	pub, amt, nonce, err := checkCommon(nodeAddr, signAddr, pubkey, addr, amtS, nonceS)
+	if err != nil {
+		return nil, err
+	}
+
+	fee, err := strconv.ParseInt(feeS, 10, 64)
+	if err != nil {
+		return nil, fmt.Errorf("fee is misformatted: %v", err)
+	}
+
+	tx := types.NewNameTxWithNonce(pub, name, data, amt, fee, int(nonce))
+	return tx, nil
+}
+
+type PermFunc struct {
+	Name string
+	Args string
+}
+
+var PermsFuncs = []PermFunc{
+	{"set_base", "address, permission flag, value"},
+	{"unset_base", "address, permission flag"},
+	{"set_global", "permission flag, value"},
+	{"add_role", "address, role"},
+	{"rm_role", "address, role"},
+}
+
+func Permissions(nodeAddr, signAddr, pubkey, addrS, nonceS, permFunc string, argsS []string) (*txs.PermissionsTx, error) {
+	pub, _, nonce, err := checkCommon(nodeAddr, signAddr, pubkey, addrS, "0", nonceS)
+	if err != nil {
+		return nil, err
+	}
+	var args ptypes.PermArgs
+	switch permFunc {
+	case "set_base":
+		addr, pF, err := decodeAddressPermFlag(argsS[0], argsS[1])
+		if err != nil {
+			return nil, err
+		}
+		if len(argsS) != 3 {
+			return nil, fmt.Errorf("set_base also takes a value (true or false)")
+		}
+		var value bool
+		if argsS[2] == "true" {
+			value = true
+		} else if argsS[2] == "false" {
+			value = false
+		} else {
+			return nil, fmt.Errorf("Unknown value %s", argsS[2])
+		}
+		args = &ptypes.SetBaseArgs{addr, pF, value}
+	case "unset_base":
+		addr, pF, err := decodeAddressPermFlag(argsS[0], argsS[1])
+		if err != nil {
+			return nil, err
+		}
+		args = &ptypes.UnsetBaseArgs{addr, pF}
+	case "set_global":
+		pF, err := ptypes.PermStringToFlag(argsS[0])
+		if err != nil {
+			return nil, err
+		}
+		var value bool
+		if argsS[1] == "true" {
+			value = true
+		} else if argsS[1] == "false" {
+			value = false
+		} else {
+			return nil, fmt.Errorf("Unknown value %s", argsS[1])
+		}
+		args = &ptypes.SetGlobalArgs{pF, value}
+	case "add_role":
+		addr, err := hex.DecodeString(argsS[0])
+		if err != nil {
+			return nil, err
+		}
+		args = &ptypes.AddRoleArgs{addr, argsS[1]}
+	case "rm_role":
+		addr, err := hex.DecodeString(argsS[0])
+		if err != nil {
+			return nil, err
+		}
+		args = &ptypes.RmRoleArgs{addr, argsS[1]}
+	default:
+		return nil, fmt.Errorf("Invalid permission function for use in PermissionsTx: %s", permFunc)
+	}
+	// args := snativeArgs(
+	tx := types.NewPermissionsTxWithNonce(pub, args, int(nonce))
+	return tx, nil
+}
+
+func decodeAddressPermFlag(addrS, permFlagS string) (addr []byte, pFlag ptypes.PermFlag, err error) {
+	if addr, err = hex.DecodeString(addrS); err != nil {
+		return
+	}
+	if pFlag, err = ptypes.PermStringToFlag(permFlagS); err != nil {
+		return
+	}
+	return
+}
+
+type NameGetter struct {
+	client cclient.Client
+}
+
+func (n NameGetter) GetNameRegEntry(name string) *txs.NameRegEntry {
+	entry, err := n.client.GetName(name)
+	if err != nil {
+		panic(err)
+	}
+	return entry.Entry
+}
+
+/*
+func coreNewAccount(nodeAddr, pubkey, chainID string) (*types.NewAccountTx, error) {
+	pub, _, _, err := checkCommon(nodeAddr, pubkey, "", "0", "0")
+	if err != nil {
+		return nil, err
+	}
+
+	client := cclient.NewClient(nodeAddr, "HTTP")
+	return types.NewNewAccountTx(NameGetter{client}, pub, chainID)
+}
+*/
+
+func Bond(nodeAddr, signAddr, pubkey, unbondAddr, amtS, nonceS string) (*txs.BondTx, error) {
+	pub, amt, nonce, err := checkCommon(nodeAddr, signAddr, pubkey, "", amtS, nonceS)
+	if err != nil {
+		return nil, err
+	}
+	var pubKey account.PubKeyEd25519
+	var unbondAddrBytes []byte
+
+	if unbondAddr == "" {
+		pkb, _ := hex.DecodeString(pubkey)
+		copy(pubKey[:], pkb)
+		unbondAddrBytes = pubKey.Address()
+	} else {
+		unbondAddrBytes, err = hex.DecodeString(unbondAddr)
+		if err != nil {
+			return nil, fmt.Errorf("unbondAddr is bad hex: %v", err)
+		}
+
+	}
+
+	tx, err := types.NewBondTx(pub)
+	if err != nil {
+		return nil, err
+	}
+	tx.AddInputWithNonce(pub, amt, int(nonce))
+	tx.AddOutput(unbondAddrBytes, amt)
+
+	return tx, nil
+}
+
+func Unbond(addrS, heightS string) (*txs.UnbondTx, error) {
+	if addrS == "" {
+		return nil, fmt.Errorf("Validator address must be given with --addr flag")
+	}
+
+	addrBytes, err := hex.DecodeString(addrS)
+	if err != nil {
+		return nil, fmt.Errorf("addr is bad hex: %v", err)
+	}
+
+	height, err := strconv.ParseInt(heightS, 10, 32)
+	if err != nil {
+		return nil, fmt.Errorf("height is misformatted: %v", err)
+	}
+
+	return &types.UnbondTx{
+		Address: addrBytes,
+		Height:  int(height),
+	}, nil
+}
+
+func Rebond(addrS, heightS string) (*txs.RebondTx, error) {
+	if addrS == "" {
+		return nil, fmt.Errorf("Validator address must be given with --addr flag")
+	}
+
+	addrBytes, err := hex.DecodeString(addrS)
+	if err != nil {
+		return nil, fmt.Errorf("addr is bad hex: %v", err)
+	}
+
+	height, err := strconv.ParseInt(heightS, 10, 32)
+	if err != nil {
+		return nil, fmt.Errorf("height is misformatted: %v", err)
+	}
+
+	return &types.RebondTx{
+		Address: addrBytes,
+		Height:  int(height),
+	}, nil
+}
+
+//------------------------------------------------------------------------------------
+// sign and broadcast
+
+func Pub(addr, rpcAddr string) (pubBytes []byte, err error) {
+	args := map[string]string{
+		"addr": addr,
+	}
+	pubS, err := RequestResponse(rpcAddr, "pub", args)
+	if err != nil {
+		return
+	}
+	return hex.DecodeString(pubS)
+}
+
+func Sign(signBytes, signAddr, signRPC string) (sig [64]byte, err error) {
+	args := map[string]string{
+		"msg":  signBytes,
+		"hash": signBytes, // backwards compatibility
+		"addr": signAddr,
+	}
+	sigS, err := RequestResponse(signRPC, "sign", args)
+	if err != nil {
+		return
+	}
+	sigBytes, err := hex.DecodeString(sigS)
+	if err != nil {
+		return
+	}
+	copy(sig[:], sigBytes)
+	return
+}
+
+func Broadcast(tx types.Tx, broadcastRPC string) (*txs.Receipt, error) {
+	client := cclient.NewClient(broadcastRPC, "JSONRPC")
+	rec, err := client.BroadcastTx(tx)
+	if err != nil {
+		return nil, err
+	}
+	return &rec.Receipt, nil
+}
+
+//------------------------------------------------------------------------------------
+// utils for talking to the key server
+
+type HTTPResponse struct {
+	Response string
+	Error    string
+}
+
+func RequestResponse(addr, method string, args map[string]string) (string, error) {
+	b, err := json.Marshal(args)
+	if err != nil {
+		return "", err
+	}
+	endpoint := fmt.Sprintf("%s/%s", addr, method)
+	logger.Debugf("Sending request body (%s): %s\n", endpoint, string(b))
+	req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(b))
+	if err != nil {
+		return "", err
+	}
+	req.Header.Add("Content-Type", "application/json")
+	res, errS, err := requestResponse(req)
+	if err != nil {
+		return "", fmt.Errorf("Error calling eris-keys at %s: %s", endpoint, err.Error())
+	}
+	if errS != "" {
+		return "", fmt.Errorf("Error (string) calling eris-keys at %s: %s", endpoint, errS)
+	}
+	return res, nil
+}
+
+func requestResponse(req *http.Request) (string, string, error) {
+	client := new(http.Client)
+	resp, err := client.Do(req)
+	if err != nil {
+		return "", "", err
+	}
+	if resp.StatusCode >= 400 {
+		return "", "", fmt.Errorf(resp.Status)
+	}
+	return unpackResponse(resp)
+}
+
+func unpackResponse(resp *http.Response) (string, string, error) {
+	b, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return "", "", err
+	}
+	r := new(HTTPResponse)
+	if err := json.Unmarshal(b, r); err != nil {
+		return "", "", err
+	}
+	return r.Response, r.Error, nil
+}
+
+//------------------------------------------------------------------------------------
+// sign and broadcast convenience
+
+// tx has either one input or we default to the first one (ie for send/bond)
+// TODO: better support for multisig and bonding
+func signTx(signAddr, chainID string, tx_ txs.Tx) ([]byte, txs.Tx, error) {
+	signBytes := fmt.Sprintf("%X", account.SignBytes(chainID, tx_))
+	var inputAddr []byte
+	var sigED account.SignatureEd25519
+	switch tx := tx_.(type) {
+	case *types.SendTx:
+		inputAddr = tx.Inputs[0].Address
+		defer func(s *account.SignatureEd25519) { tx.Inputs[0].Signature = *s }(&sigED)
+	case *types.NameTx:
+		inputAddr = tx.Input.Address
+		defer func(s *account.SignatureEd25519) { tx.Input.Signature = *s }(&sigED)
+	case *types.CallTx:
+		inputAddr = tx.Input.Address
+		defer func(s *account.SignatureEd25519) { tx.Input.Signature = *s }(&sigED)
+	case *types.PermissionsTx:
+		inputAddr = tx.Input.Address
+		defer func(s *account.SignatureEd25519) { tx.Input.Signature = *s }(&sigED)
+	case *types.BondTx:
+		inputAddr = tx.Inputs[0].Address
+		defer func(s *account.SignatureEd25519) {
+			tx.Signature = *s
+			tx.Inputs[0].Signature = *s
+		}(&sigED)
+	case *types.UnbondTx:
+		inputAddr = tx.Address
+		defer func(s *account.SignatureEd25519) { tx.Signature = *s }(&sigED)
+	case *types.RebondTx:
+		inputAddr = tx.Address
+		defer func(s *account.SignatureEd25519) { tx.Signature = *s }(&sigED)
+	}
+	addrHex := fmt.Sprintf("%X", inputAddr)
+	sig, err := Sign(signBytes, addrHex, signAddr)
+	if err != nil {
+		return nil, nil, err
+	}
+	sigED = account.SignatureEd25519(sig)
+	logger.Debugf("SIG: %X\n", sig)
+	return inputAddr, tx_, nil
+}
+
+type TxResult struct {
+	BlockHash []byte // all txs get in a block
+	Hash      []byte // all txs get a hash
+
+	// only CallTx
+	Address   []byte // only for new contracts
+	Return    []byte
+	Exception string
+
+	//TODO: make Broadcast() errors more responsive so we
+	// can differentiate mempool errors from other
+}
+
+func SignAndBroadcast(chainID, nodeAddr, signAddr string, tx types.Tx, sign, broadcast, wait bool) (txResult *TxResult, err error) {
+	var inputAddr []byte
+	if sign {
+		inputAddr, tx, err = signTx(signAddr, chainID, tx)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if broadcast {
+		if wait {
+			var ch chan Msg
+			ch, err = subscribeAndWait(tx, chainID, nodeAddr, inputAddr)
+			if err != nil {
+				return nil, err
+			} else {
+				defer func() {
+					if err != nil {
+						// if broadcast threw an error, just return
+						return
+					}
+					logger.Debugln("Waiting for tx to be committed ...")
+					msg := <-ch
+					if msg.Error != nil {
+						logger.Infof("Encountered error waiting for event: %v\n", msg.Error)
+						err = msg.Error
+					} else {
+						txResult.BlockHash = msg.BlockHash
+						txResult.Return = msg.Value
+						txResult.Exception = msg.Exception
+					}
+				}()
+			}
+		}
+		var receipt *rtypes.Receipt
+		receipt, err = Broadcast(tx, nodeAddr)
+		if err != nil {
+			return nil, err
+		}
+		txResult = &TxResult{
+			Hash: receipt.TxHash,
+		}
+		if tx_, ok := tx.(*types.CallTx); ok {
+			if len(tx_.Address) == 0 {
+				txResult.Address = types.NewContractAddress(tx_.Input.Address, tx_.Input.Sequence)
+			}
+		}
+	}
+	return
+}
+
+//------------------------------------------------------------------------------------
+// wait for events
+
+type Msg struct {
+	BlockHash []byte
+	Value     []byte
+	Exception string
+	Error     error
+}
+
+func subscribeAndWait(tx types.Tx, chainID, nodeAddr string, inputAddr []byte) (chan Msg, error) {
+	// subscribe to event and wait for tx to be committed
+	wsAddr := strings.TrimPrefix(nodeAddr, "http://")
+	wsAddr = "ws://" + wsAddr + "websocket"
+	logger.Debugf("Websocket Address %s\n", wsAddr)
+	wsClient := cclient.NewWSClient(wsAddr)
+	wsClient.Start()
+	eid := types.EventStringAccInput(inputAddr)
+	if err := wsClient.Subscribe(eid); err != nil {
+		return nil, fmt.Errorf("Error subscribing to AccInput event: %v", err)
+	}
+	if err := wsClient.Subscribe(types.EventStringNewBlock()); err != nil {
+		return nil, fmt.Errorf("Error subscribing to NewBlock event: %v", err)
+	}
+
+	resultChan := make(chan Msg, 1)
+
+	var latestBlockHash []byte
+
+	// Read message
+	go func() {
+		for {
+			result := <-wsClient.EventsCh
+			// if its a block, remember the block hash
+			blockData, ok := result.Data.(types.EventDataNewBlock)
+			if ok {
+				logger.Infoln(blockData.Block)
+				latestBlockHash = blockData.Block.Hash()
+				continue
+			}
+
+			// we don't accept events unless they came after a new block (ie. in)
+			if latestBlockHash == nil {
+				continue
+			}
+
+			if result.Event != eid {
+				logger.Debugf("received unsolicited event! Got %s, expected %s\n", result.Event, eid)
+				continue
+			}
+
+			data, ok := result.Data.(types.EventDataTx)
+			if !ok {
+				resultChan <- Msg{Error: fmt.Errorf("response error: expected result.Data to be *types.EventDataTx")}
+				return
+			}
+
+			if !bytes.Equal(types.TxID(chainID, data.Tx), types.TxID(chainID, tx)) {
+				logger.Debugf("Received event for same input from another transaction: %X\n", types.TxID(chainID, data.Tx))
+				continue
+			}
+
+			if data.Exception != "" {
+				resultChan <- Msg{BlockHash: latestBlockHash, Value: data.Return, Exception: data.Exception}
+				return
+			}
+
+			// GOOD!
+			resultChan <- Msg{BlockHash: latestBlockHash, Value: data.Return}
+			return
+		}
+	}()
+
+	// txs should take no more than 10 seconds
+	timeoutTicker := time.Tick(time.Duration(MaxCommitWaitTimeSeconds) * time.Second)
+
+	go func() {
+		<-timeoutTicker
+		resultChan <- Msg{Error: fmt.Errorf("timed out waiting for event")}
+		return
+	}()
+	return resultChan, nil
+}
+
+//------------------------------------------------------------------------------------
+// convenience function
+
+func checkCommon(nodeAddr, signAddr, pubkey, addr, amtS, nonceS string) (pub account.PubKey, amt int64, nonce int64, err error) {
+	if amtS == "" {
+		err = fmt.Errorf("input must specify an amount with the --amt flag")
+		return
+	}
+
+	var pubKeyBytes []byte
+	if pubkey == "" && addr == "" {
+		err = fmt.Errorf("at least one of --pubkey or --addr must be given")
+		return
+	} else if pubkey != "" {
+		if addr != "" {
+			// NOTE: if --addr given byt MINTX_PUBKEY is set, the pubkey still wins
+			// TODO: fix this
+			logger.Infoln("you have specified both a pubkey and an address. the pubkey takes precedent")
+		}
+		pubKeyBytes, err = hex.DecodeString(pubkey)
+		if err != nil {
+			err = fmt.Errorf("pubkey is bad hex: %v", err)
+			return
+		}
+	} else {
+		// grab the pubkey from eris-keys
+		pubKeyBytes, err = Pub(addr, signAddr)
+		if err != nil {
+			err = fmt.Errorf("failed to fetch pubkey for address (%s): %v", addr, err)
+			return
+		}
+
+	}
+
+	if len(pubKeyBytes) == 0 {
+		err = fmt.Errorf("Error resolving public key")
+		return
+	}
+
+	amt, err = strconv.ParseInt(amtS, 10, 64)
+	if err != nil {
+		err = fmt.Errorf("amt is misformatted: %v", err)
+	}
+
+	var pubArray [32]byte
+	copy(pubArray[:], pubKeyBytes)
+	pub = account.PubKeyEd25519(pubArray)
+	addrBytes := pub.Address()
+
+	if nonceS == "" {
+		if nodeAddr == "" {
+			err = fmt.Errorf("input must specify a nonce with the --nonce flag or use --node-addr (or MINTX_NODE_ADDR) to fetch the nonce from a node")
+			return
+		}
+
+		// fetch nonce from node
+		client := cclient.NewClient(nodeAddr, "HTTP")
+		ac, err2 := client.GetAccount(addrBytes)
+		if err2 != nil {
+			err = fmt.Errorf("Error connecting to node (%s) to fetch nonce: %s", nodeAddr, err2.Error())
+			return
+		}
+		if ac == nil || ac.Account == nil {
+			err = fmt.Errorf("unknown account %X", addrBytes)
+			return
+		}
+		nonce = int64(ac.Account.Sequence) + 1
+	} else {
+		nonce, err = strconv.ParseInt(nonceS, 10, 64)
+		if err != nil {
+			err = fmt.Errorf("nonce is misformatted: %v", err)
+			return
+		}
+	}
+
+	return
+}
diff --git a/client/transaction/transaction.go b/client/transaction/transaction.go
new file mode 100644
index 00000000..8ac3150c
--- /dev/null
+++ b/client/transaction/transaction.go
@@ -0,0 +1,35 @@
+// Copyright 2015, 2016 Eris Industries (UK) Ltd.
+// This file is part of Eris-RT
+
+// Eris-RT is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Eris-RT is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Eris-RT.  If not, see <http://www.gnu.org/licenses/>.
+
+package transaction
+
+import (
+	"fmt"
+	"io/ioutil"
+
+	"github.com/eris-ltd/eris-logger"
+
+	"github.com/eris-ltd/eris-db/client/core"
+	"github.com/eris-ltd/eris-db/definitions"
+)
+
+func transactionSend(do *definitions.ClientDo) {
+	transaction, err := core.Send(do.nodeAddrFlag, do.signAddrFlag,
+		do.pubkeyFlag, do.toFlag, do.amtFlag, do.nonceFlag)
+	if err != nil {
+		log.Fatalf("Failed on Send Transaction: %s", err)
+	}
+}
\ No newline at end of file
diff --git a/cmd/eris-db.go b/cmd/eris-db.go
index 36962325..4894bd29 100644
--- a/cmd/eris-db.go
+++ b/cmd/eris-db.go
@@ -34,8 +34,8 @@ var do *definitions.Do
 
 var ErisDbCmd = &cobra.Command{
 	Use:   "eris-db",
-	Short: "Eris-DB is the heart of the eris chain.",
-	Long: `Eris-DB is the heart of the eris chain.  Eris-DB combines
+	Short: "Eris-DB is the server side of the eris chain.",
+	Long: `Eris-DB is the server side of the eris chain.  Eris-DB combines
 a modular consensus engine and application manager to run a chain to suit
 your needs.
 
@@ -56,15 +56,15 @@ Complete documentation is available at https://docs.erisindustries.com
 }
 
 func Execute() {
-	InitErisDb()
+	InitErisDbCli()
 	AddGlobalFlags()
 	AddCommands()
 	ErisDbCmd.Execute()
 }
 
-func InitErisDb() {
-	// initialise an empty do struct for command execution
-	do = definitions.NowDo()
+func InitErisDbCli() {
+	// initialise an empty Do struct for command execution
+	do = definitions.NewDo()
 }
 
 func AddGlobalFlags() {
diff --git a/definitions/client_do.go b/definitions/client_do.go
new file mode 100644
index 00000000..0b3dc562
--- /dev/null
+++ b/definitions/client_do.go
@@ -0,0 +1,78 @@
+// Copyright 2015, 2016 Eris Industries (UK) Ltd.
+// This file is part of Eris-RT
+
+// Eris-RT is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Eris-RT is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Eris-RT.  If not, see <http://www.gnu.org/licenses/>.
+
+package definitions
+
+type ClientDo {
+	// Persistent flags not reflected in the configuration files
+	// only set through command line flags or environment variables
+	Debug   bool // ERIS_DB_DEBUG
+	Verbose bool // ERIS_DB_VERBOSE
+
+	// Following parameters are global flags for eris-client
+	signAddrFlag string
+	nodeAddrFlag string
+	pubkeyFlag   string
+	addrFlag     string
+	chainidFlag  string
+
+	// signFlag      bool // TODO: remove; unsafe signing without eris-keys
+	broadcastFlag bool
+	waitFlag      bool
+
+	// Following parameters are specific for Transaction command
+	// some of these are strings rather than flags because the `core`
+	// functions have a pure string interface so they work nicely from http
+	amtFlag      string
+	nonceFlag    string
+	nameFlag     string
+	dataFlag     string
+	dataFileFlag string
+	toFlag       string
+	feeFlag      string
+	gasFlag      string
+	unbondtoFlag string
+	heightFlag   string
+}
+
+func NewClientDo() *ClientDo {
+	clientDo := new(ClientDo)
+	clientDo.Debug = false
+	clientDo.Verbose = false
+	
+	clientDo.signAddrFlag = ""
+	clientDo.nodeAddrFlag = ""
+	clientDo.pubkeyFlag = ""
+	clientDo.addrFlag = ""
+	clientDo.chainidFlag = ""
+
+	clientDo.signFlag = false
+	clientDo.broadcastFlag = false
+	clientDo.waitFlag = false
+
+	clientDo.amtFlag = ""
+	clientDo.nonceFlag = ""
+	clientDo.nameFlag = ""
+	clientDo.dataFlag = ""
+	clientDo.dataFileFlag = ""
+	clientDo.toFlag = ""
+	clientDo.feeFlag = ""
+	clientDo.gasFlag = ""
+	clientDo.unbondtoFlag = ""
+	clientDo.heightFlag = ""
+
+	return clientDo
+}
\ No newline at end of file
diff --git a/definitions/do.go b/definitions/do.go
index 0ad4e108..25966bdd 100644
--- a/definitions/do.go
+++ b/definitions/do.go
@@ -53,7 +53,7 @@ type Do struct {
 	// Result       string
 }
 
-func NowDo() *Do {
+func NewDo() *Do {
 	do := new(Do)
 	do.Debug = false
 	do.Verbose = false
-- 
GitLab