cmd/signer: refactors, removed channels in ui comms, added UI-api via stdin/out

This commit is contained in:
Martin Holst Swende 2017-12-06 12:11:19 +01:00
parent d17a51829d
commit d0c0b9bacd
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
8 changed files with 605 additions and 243 deletions

View file

@ -25,6 +25,7 @@ The signer accepts the following command line options:
--rpcaddr value HTTP-RPC server listening interface (default: "localhost") --rpcaddr value HTTP-RPC server listening interface (default: "localhost")
--rpcport value HTTP-RPC server listening port (default: 8550) --rpcport value HTTP-RPC server listening port (default: 8550)
--4bytedb value File containing 4byte-identifiers (default: "./4byte.json") --4bytedb value File containing 4byte-identifiers (default: "./4byte.json")
--stdio-ui Use STDIN/STDOUT as a channel for an external UI. This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user interface, and can be used when the signer is started by an external process.
--help, -h show help --help, -h show help
``` ```
@ -34,15 +35,39 @@ Example:
signer -keystore /my/keystore -chainid 4 signer -keystore /my/keystore -chainid 4
``` ```
## Communicating ## Communication
The signer listens to HTTP requests on `rpcaddr`:`rpcport`. The messages are expected to be JSON ### External API
[jsonrpc 2.0 standard](http://www.jsonrpc.org/specification).
Some of these call can require user interaction. Clients must be aware that responses may be deplayed significanlty or The signer listens to HTTP requests on `rpcaddr`:`rpcport`. The messages are
may never be received if a users decideds to ignore the confirmation request. expected to be JSON [jsonrpc 2.0 standard](http://www.jsonrpc.org/specification).
## API Some of these call can require user interaction. Clients must be aware that responses
may be deplayed significanlty or may never be received if a users decideds to ignore the confirmation request.
The External API is **untrusted** : it does not accept credentials over this api, nor does it expect
that requests have any authority.
### UI API
The signer has one native console-based UI, for operation without any standalone tools.
However, there is also an API to communicate with an external UI. To enable that UI,
the signer needs to be executed with the `--stdio-ui` option, which allocates the
`stdin`/`stdout` for the UI-api.
An example (insecure) proof-of-concept of has been implemented in `pythonsigner.py`.
The model is as follows:
* The user starts the UI app (`pythonsigner.py`).
* The UI app starts the `signer` with `--stdio-ui`, and listens to the
process output for confirmation-requests.
* The `signer` opens the external http api.
* When the `signer` receives requests, it sends a `jsonrpc` request via `stdout`.
* The UI app prompts the user accordingly, and responds to the `signer`
* The `signer` signs (or not), and responds to the original request.
## External API
### Encoding ### Encoding
- number: positive integers that are hex encoded - number: positive integers that are hex encoded
@ -348,3 +373,8 @@ None
## UI API
These methods needs to be implemented by a UI listener.
still work in progress

View file

@ -24,6 +24,7 @@ import (
"io/ioutil" "io/ioutil"
"math/big" "math/big"
"bytes"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet"
@ -51,57 +52,63 @@ type Metadata struct {
// types for the requests/response types // types for the requests/response types
type ( type (
// SignTxRequest contains info about a transaction to sign // SignTxRequest contains info about a Transaction to sign
SignTxRequest struct { SignTxRequest struct {
transaction types.Transaction Transaction TransactionArg
from accounts.Account From common.Address
callinfo fmt.Stringer Callinfo fmt.Stringer
Meta Metadata
} }
// SignTxResponse result from SignTxRequest // SignTxResponse result from SignTxRequest
SignTxResponse struct { SignTxResponse struct {
//The UI may make changes to the TX //The UI may make changes to the TX
transaction types.Transaction Transaction TransactionArg
approved bool From common.Address
pw string Approved bool
Password string
} }
// ExportRequest info about query to export accounts // ExportRequest info about query to export accounts
ExportRequest struct { ExportRequest struct {
account accounts.Account Address common.Address
file string Meta Metadata
} }
// ExportResponse response to export-request // ExportResponse response to export-request
ExportResponse struct { ExportResponse struct {
approved bool Approved bool
} }
// ImportRequest info about request to import an account // ImportRequest info about request to import an Account
ImportRequest struct { ImportRequest struct {
account accounts.Account Meta Metadata
} }
ImportResponse struct { ImportResponse struct {
approved bool Approved bool
oldPassword string OldPassword string
newPassword string NewPassword string
} }
SignDataRequest struct { SignDataRequest struct {
account accounts.Account Address common.Address
rawdata hexutil.Bytes Rawdata hexutil.Bytes
message string Message string
hash hexutil.Bytes Hash hexutil.Bytes
Meta Metadata
} }
SignDataResponse struct { SignDataResponse struct {
approved bool Approved bool
pw string Password string
}
NewAccountRequest struct {
Meta Metadata
} }
NewAccountRequest struct{}
NewAccountResponse struct { NewAccountResponse struct {
approved bool Approved bool
pw string Password string
} }
ListRequest struct { ListRequest struct {
accounts []Account Accounts []Account
Meta Metadata
} }
ListResponse struct { ListResponse struct {
accounts []Account Accounts []Account
} }
) )
@ -120,28 +127,28 @@ func (ew errorWrapper) String() string {
// for the signer // for the signer
type SignerUI interface { type SignerUI interface {
// ApproveTx prompt the user for confirmation to request to sign transaction // ApproveTx prompt the user for confirmation to request to sign Transaction
ApproveTx(request *SignTxRequest, metadata Metadata, ch chan SignTxResponse) ApproveTx(request *SignTxRequest) (SignTxResponse, error)
// ApproveSignData prompt the user for confirmation to request to sign data // ApproveSignData prompt the user for confirmation to request to sign data
ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan SignDataResponse) ApproveSignData(request *SignDataRequest) (SignDataResponse, error)
// ApproveExport prompt the user for confirmation to export encrypted account json // ApproveExport prompt the user for confirmation to export encrypted Account json
ApproveExport(request *ExportRequest, metadata Metadata, ch chan ExportResponse) ApproveExport(request *ExportRequest) (ExportResponse, error)
// ApproveImport prompt the user for confirmation to import account json // ApproveImport prompt the user for confirmation to import Account json
ApproveImport(request *ImportRequest, metadata Metadata, ch chan ImportResponse) ApproveImport(request *ImportRequest) (ImportResponse, error)
// ApproveListing prompt the user for confirmation to list accounts // ApproveListing prompt the user for confirmation to list accounts
// the list of accounts to list can be modified by the ui // the list of accounts to list can be modified by the ui
ApproveListing(request *ListRequest, metadata Metadata, ch chan ListResponse) ApproveListing(request *ListRequest) (ListResponse, error)
// ApproveNewAccount prompt the user for confirmation to create new account, and reveal to caller // ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller
ApproveNewAccount(requst *NewAccountRequest, metadata Metadata, ch chan NewAccountResponse) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error)
// ShowError displays error message to user // ShowError displays error message to user
ShowError(message string) ShowError(message string)
// ShowInfo displays info message to user // ShowInfo displays info message to user
ShowInfo(message string) ShowInfo(message string)
} }
// NewSignerAPI creates a new API that can be used for account management. // NewSignerAPI creates a new API that can be used for Account management.
// ksLocation specifies the directory where to store the password protected private // ksLocation specifies the directory where to store the password protected private
// key that is generated when a new account is created. // key that is generated when a new Account is created.
// noUSB disables USB support that is required to support hardware devices such as // noUSB disables USB support that is required to support hardware devices such as
// ledger and trezor. // ledger and trezor.
func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI, abidb *abiDb, lightKDF bool) *SignerAPI { func NewSignerAPI(chainID int64, ksLocation string, noUSB bool, ui SignerUI, abidb *abiDb, lightKDF bool) *SignerAPI {
@ -194,23 +201,26 @@ func metaData(ctx context.Context) Metadata {
// multiple accounts. // multiple accounts.
func (api *SignerAPI) List(ctx context.Context) (Accounts, error) { func (api *SignerAPI) List(ctx context.Context) (Accounts, error) {
ch := make(chan ListResponse, 1)
var accs []Account var accs []Account
for _, wallet := range api.am.Wallets() { for _, wallet := range api.am.Wallets() {
for _, acc := range wallet.Accounts() { for _, acc := range wallet.Accounts() {
acc := Account{Typ: "account", URL: wallet.URL(), Address: acc.Address} acc := Account{Typ: "Account", URL: wallet.URL(), Address: acc.Address}
accs = append(accs, acc) accs = append(accs, acc)
} }
} }
api.ui.ApproveListing(&ListRequest{accounts: accs}, metaData(ctx), ch) result, err := api.ui.ApproveListing(&ListRequest{Accounts: accs, Meta: metaData(ctx)})
if result := <-ch; result.accounts != nil { if err != nil {
return result.accounts, nil return nil, err
} }
return nil, ErrRequestDenied if result.Accounts == nil {
return nil, ErrRequestDenied
}
return result.Accounts, nil
} }
// New creates a new password protected account. The private key is protected with // New creates a new password protected Account. The private key is protected with
// the given password. Users are responsible to backup the private key that is stored // the given password. Users are responsible to backup the private key that is stored
// in the keystore location thas was specified when this API was created. // in the keystore location thas was specified when this API was created.
func (api *SignerAPI) New(ctx context.Context) (accounts.Account, error) { func (api *SignerAPI) New(ctx context.Context) (accounts.Account, error) {
@ -218,72 +228,128 @@ func (api *SignerAPI) New(ctx context.Context) (accounts.Account, error) {
if len(be) == 0 { if len(be) == 0 {
return accounts.Account{}, errors.New("password based accounts not supported") return accounts.Account{}, errors.New("password based accounts not supported")
} }
ch := make(chan NewAccountResponse, 1) resp, err := api.ui.ApproveNewAccount(&NewAccountRequest{metaData(ctx)})
api.ui.ApproveNewAccount(&NewAccountRequest{}, metaData(ctx), ch)
if resp := <-ch; resp.approved {
return be[0].(*keystore.KeyStore).NewAccount(resp.pw)
if err != nil {
return accounts.Account{}, err
} }
return accounts.Account{}, ErrRequestDenied if !resp.Approved {
return accounts.Account{}, ErrRequestDenied
}
return be[0].(*keystore.KeyStore).NewAccount(resp.Password)
} }
// SignTransaction signs the given transaction and returns it in an RLP encoded form func toTransaction(args *TransactionArg) *types.Transaction {
if args.To == nil {
return types.NewContractCreation(uint64(*args.Nonce), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data)
} else {
return types.NewTransaction(uint64(*args.Nonce), *args.To, (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data)
}
}
// logDiff logs the difference between the incoming (original) transaction and the one returned from the signer.
// it also returns 'true' if the transaction was modified, to make it possible to configure the signer not to allow
// UI-modifications to requests
func logDiff(original *SignTxRequest, new *SignTxResponse) bool {
modified := false
if f0, f1 := original.From, new.From; f0 != f1 {
modified = true
log.Info("Sender-account changed by UI", "was", f0, "is", f1)
}
if t0, t1 := original.Transaction.To, new.Transaction.To; t0 != t1 {
if t0 == nil || t1 == nil || !bytes.Equal(t0.Bytes(), t1.Bytes()) {
log.Info("Recipient-account changed by UI", "was", t0, "is", t1)
modified = true
}
}
if g0, g1 := (*big.Int)(original.Transaction.Gas), (*big.Int)(new.Transaction.Gas); g0 != g1 {
if g0 == nil || g1 == nil || g0.Cmp(g1) != 0 {
modified = true
log.Info("Gas changed by UI", "was", g0, "is", g1)
}
}
if g0, g1 := (*big.Int)(original.Transaction.GasPrice), (*big.Int)(new.Transaction.GasPrice); g0 != g1 {
if g0 == nil || g1 == nil || g0.Cmp(g1) != 0 {
modified = true
log.Info("GasPrice changed by UI", "was", g0, "is", g1)
}
}
if v0, v1 := (*big.Int)(original.Transaction.Value), (*big.Int)(new.Transaction.Value); v0 != v1 {
if v0 == nil || v1 == nil || v0.Cmp(v1) != 0 {
modified = true
log.Info("Value changed by UI", "was", v0, "is", v1)
}
}
if d0, d1 := original.Transaction.Data, new.Transaction.Data; !bytes.Equal(d0, d1) {
modified = true
log.Info("Data changed by UI", "was", common.ToHex(d0), "is", common.ToHex(d1))
}
if n0, n1 := original.Transaction.Nonce, new.Transaction.Nonce; n0 != n1 {
if n0 == nil || n1 == nil || (*n0) != (*n1) {
modified = true
log.Info("Nonce changed by UI", "was", n0, "is", n1)
}
}
return modified
}
// SignTransaction signs the given Transaction and returns it in an RLP encoded form
// that can be posted to `eth_sendRawTransaction`. // that can be posted to `eth_sendRawTransaction`.
func (api *SignerAPI) SignTransaction(ctx context.Context, from common.Address, args TransactionArg, methodSig *string) (hexutil.Bytes, error) { func (api *SignerAPI) SignTransaction(ctx context.Context, from common.Address, args TransactionArg, methodSig *string) (hexutil.Bytes, error) {
acc := accounts.Account{Address: from}
wallet, err := api.am.Find(acc) var (
if err != nil { err error
return nil, err result SignTxResponse
} )
var tx *types.Transaction req := SignTxRequest{Transaction: args, From: from, Meta: metaData(ctx)}
if args.To == nil {
tx = types.NewContractCreation(uint64(*args.Nonce), (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data)
} else {
tx = types.NewTransaction(uint64(*args.Nonce), *args.To, (*big.Int)(args.Value), (*big.Int)(args.Gas), (*big.Int)(args.GasPrice), args.Data)
}
req := SignTxRequest{transaction: *tx, from: acc} data := args.Data
if len(tx.Data()) > 3 { if len(data) > 3 {
// Try to make sense of the data // Try to make sense of the data
var abidata string var abidata string
if methodSig == nil { if methodSig == nil {
abidata, err = api.abidb.LookupABI(tx.Data()[:4]) abidata, err = api.abidb.LookupABI(data[:4])
if err != nil { if err != nil {
req.callinfo = errorWrapper{"Warning! Could not locate ABI", err} req.Callinfo = errorWrapper{"Warning! Could not locate ABI", err}
} }
} else { } else {
abidata = *methodSig abidata = *methodSig
} }
if abidata != "" { if abidata != "" {
req.callinfo, err = parseCallData(tx.Data(), abidata) req.Callinfo, err = parseCallData(data, abidata)
if err != nil { if err != nil {
req.callinfo = errorWrapper{"Warning! Could not validate ABI-data against calldata", err} req.Callinfo = errorWrapper{"Warning! Could not validate ABI-data against calldata", err}
} }
} }
} }
ch := make(chan SignTxResponse, 1) result, err = api.ui.ApproveTx(&req)
api.ui.ApproveTx(&req, metaData(ctx), ch) if err != nil {
return nil, err
}
if !result.Approved {
return nil, ErrRequestDenied
}
// Log changes made by the UI to the signing-request
logDiff(&req, &result)
if result := <-ch; result.approved { acc := accounts.Account{Address: result.From}
//Sanity check wallet, err := api.am.Find(acc)
if result.transaction.Hash() != tx.Hash() { if err != nil {
api.ui.ShowInfo("Transaction modified by UI") return nil, err
}
// The one to sign is the one that was returned from the UI
signedTx, err := wallet.SignTxWithPassphrase(acc, result.pw, &result.transaction, api.chainID)
if err != nil {
api.ui.ShowError(err.Error())
return nil, err
}
return rlp.EncodeToBytes(signedTx)
} }
return nil, ErrRequestDenied var tx = toTransaction(&result.Transaction)
// The one to sign is the one that was returned from the UI
signedTx, err := wallet.SignTxWithPassphrase(acc, result.Password, tx, api.chainID)
if err != nil {
api.ui.ShowError(err.Error())
return nil, err
}
return rlp.EncodeToBytes(signedTx)
} }
// Sign calculates an Ethereum ECDSA signature for: // Sign calculates an Ethereum ECDSA signature for:
@ -296,35 +362,39 @@ func (api *SignerAPI) SignTransaction(ctx context.Context, from common.Address,
// //
// https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign // https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign
func (api *SignerAPI) Sign(ctx context.Context, addr common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { func (api *SignerAPI) Sign(ctx context.Context, addr common.Address, data hexutil.Bytes) (hexutil.Bytes, error) {
sighash, msg := signHash(data)
// We make the request prior to looking up if we actually have the account, to prevent
// account-enumeration via the API
req := &SignDataRequest{Address: addr, Rawdata: data, Message: msg, Hash: sighash, Meta: metaData(ctx)}
res, err := api.ui.ApproveSignData(req)
if err != nil {
return nil, err
}
if !res.Approved {
return nil, ErrRequestDenied
}
// Look up the wallet containing the requested signer // Look up the wallet containing the requested signer
account := accounts.Account{Address: addr} account := accounts.Account{Address: addr}
wallet, err := api.am.Find(account) wallet, err := api.am.Find(account)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ch := make(chan SignDataResponse, 1) // Assemble sign the data with the wallet
signature, err := wallet.SignHashWithPassphrase(account, res.Password, sighash)
sighash, msg := signHash(data) if err != nil {
api.ui.ShowError(err.Error())
api.ui.ApproveSignData(&SignDataRequest{account: account, rawdata: data, message: msg, hash: sighash}, metaData(ctx), ch) return nil, err
if res := <-ch; res.approved {
// Assemble sign the data with the wallet
signature, err := wallet.SignHashWithPassphrase(account, res.pw, sighash)
if err != nil {
api.ui.ShowError(err.Error())
return nil, err
}
signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
return signature, nil
} }
return nil, ErrRequestDenied signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper
return signature, nil
} }
// EcRecover returns the address for the account that was used to create the signature. // EcRecover returns the address for the Account that was used to create the signature.
// Note, this function is compatible with eth_sign and personal_sign. As such it recovers // Note, this function is compatible with eth_sign and personal_sign. As such it recovers
// the address of: // the address of:
// hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message}) // hash = keccak256("\x19Ethereum Signed Message:\n"${message length}${message})
@ -367,26 +437,27 @@ func signHash(data []byte) ([]byte, string) {
// Export returns encrypted private key associated with the given address in web3 keystore format. // Export returns encrypted private key associated with the given address in web3 keystore format.
func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) { func (api *SignerAPI) Export(ctx context.Context, addr common.Address) (json.RawMessage, error) {
// Look up the wallet containing the requested signer
account := accounts.Account{Address: addr}
wallet, err := api.am.Find(account) res, err := api.ui.ApproveExport(&ExportRequest{Address: addr, Meta: metaData(ctx)})
if err != nil {
return nil, err
}
if !res.Approved {
return nil, ErrRequestDenied
}
// Look up the wallet containing the requested signer
wallet, err := api.am.Find(accounts.Account{Address: addr})
if err != nil { if err != nil {
return nil, err return nil, err
} }
url := wallet.URL() if wallet.URL().Scheme != keystore.KeyStoreScheme {
if url.Scheme != keystore.KeyStoreScheme { return nil, fmt.Errorf("Account is not a keystore-account")
return nil, fmt.Errorf("account is not a password protected account")
} }
ch := make(chan ExportResponse, 1)
api.ui.ApproveExport(&ExportRequest{account: account, file: url.Path}, metaData(ctx), ch) return ioutil.ReadFile(wallet.URL().Path)
if (<-ch).approved {
return ioutil.ReadFile(url.Path)
}
return nil, ErrRequestDenied
} }
// Imports tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be // Imports tries to import the given keyJSON in the local keystore. The keyJSON data is expected to be
@ -400,20 +471,21 @@ func (api *SignerAPI) Import(ctx context.Context, string, keyJSON json.RawMessag
return Account{}, errors.New("password based accounts not supported") return Account{}, errors.New("password based accounts not supported")
} }
ch := make(chan ImportResponse, 1) res, err := api.ui.ApproveImport(&ImportRequest{Meta: metaData(ctx)})
api.ui.ApproveImport(&ImportRequest{}, metaData(ctx), ch) if err != nil {
return Account{}, err
if resp := <-ch; resp.approved {
acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, resp.oldPassword, resp.newPassword)
if err != nil {
api.ui.ShowError(err.Error())
return Account{}, err
}
return Account{Typ: "account", URL: acc.URL, Address: acc.Address}, nil
} }
return Account{}, ErrRequestDenied if !res.Approved {
return Account{}, ErrRequestDenied
}
acc, err := be[0].(*keystore.KeyStore).Import(keyJSON, res.OldPassword, res.NewPassword)
if err != nil {
api.ui.ShowError(err.Error())
return Account{}, err
}
return Account{Typ: "Account", URL: acc.URL, Address: acc.Address}, nil
} }

View file

@ -8,7 +8,6 @@ import (
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"io/ioutil" "io/ioutil"
"math/big" "math/big"
"os" "os"
@ -22,70 +21,57 @@ type HeadlessUI struct {
controller chan string controller chan string
} }
func (ui *HeadlessUI) ApproveTx(request *SignTxRequest, metadata Metadata, ch chan SignTxResponse) { func (ui *HeadlessUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
switch <-ui.controller { switch <-ui.controller {
case "Y": case "Y":
ch <- SignTxResponse{request.transaction, true, <-ui.controller} return SignTxResponse{request.Transaction, request.From, true, <-ui.controller}, nil
case "M": //Modify case "M": //Modify
old := request.transaction old := (*big.Int)(request.Transaction.Value)
newVal := big.NewInt(0).Add(old.Value(), big.NewInt(1)) newVal := big.NewInt(0).Add(old, big.NewInt(1))
tx := types.NewTransaction(old.Nonce(), *old.To(), newVal, old.Gas(), old.GasPrice(), old.Data()) request.Transaction.Value = (*hexutil.Big)(newVal)
ch <- SignTxResponse{*tx, true, <-ui.controller} return SignTxResponse{request.Transaction, request.From, true, <-ui.controller}, nil
default: default:
ch <- SignTxResponse{request.transaction, false, ""} return SignTxResponse{request.Transaction, request.From, false, ""}, nil
} }
} }
func (ui *HeadlessUI) ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan SignDataResponse) { func (ui *HeadlessUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
switch <-ui.controller { if "Y" == <-ui.controller {
case "Y": return SignDataResponse{true, <-ui.controller}, nil
ch <- SignDataResponse{true, <-ui.controller}
default:
ch <- SignDataResponse{false, ""}
} }
return SignDataResponse{false, ""}, nil
} }
func (ui *HeadlessUI) ApproveExport(request *ExportRequest, metadata Metadata, ch chan ExportResponse) { func (ui *HeadlessUI) ApproveExport(request *ExportRequest) (ExportResponse, error) {
switch <-ui.controller { return ExportResponse{<-ui.controller == "Y"}, nil
case "Y":
ch <- ExportResponse{true}
default:
ch <- ExportResponse{false}
}
} }
func (ui *HeadlessUI) ApproveImport(request *ImportRequest, metadata Metadata, ch chan ImportResponse) { func (ui *HeadlessUI) ApproveImport(request *ImportRequest) (ImportResponse, error) {
switch <-ui.controller { if "Y" == <-ui.controller {
case "Y": return ImportResponse{true, <-ui.controller, <-ui.controller}, nil
ch <- ImportResponse{true, <-ui.controller, <-ui.controller}
default:
ch <- ImportResponse{false, "", ""}
} }
return ImportResponse{false, "", ""}, nil
} }
func (ui *HeadlessUI) ApproveListing(request *ListRequest, metadata Metadata, ch chan ListResponse) { func (ui *HeadlessUI) ApproveListing(request *ListRequest) (ListResponse, error) {
switch <-ui.controller { switch <-ui.controller {
case "A": case "A":
ch <- ListResponse{request.accounts} return ListResponse{request.Accounts}, nil
case "1": case "1":
l := make([]Account, 1) l := make([]Account, 1)
l[0] = request.accounts[1] l[0] = request.Accounts[1]
ch <- ListResponse{l} return ListResponse{l}, nil
default: default:
ch <- ListResponse{nil} return ListResponse{nil}, nil
} }
} }
func (ui *HeadlessUI) ApproveNewAccount(requst *NewAccountRequest, metadata Metadata, ch chan NewAccountResponse) { func (ui *HeadlessUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
switch <-ui.controller { if "Y" == <-ui.controller {
case "Y": return NewAccountResponse{true, <-ui.controller}, nil
ch <- NewAccountResponse{true, <-ui.controller}
default:
ch <- NewAccountResponse{false, ""}
} }
return NewAccountResponse{false, ""}, nil
} }
func (ui *HeadlessUI) ShowError(message string) { func (ui *HeadlessUI) ShowError(message string) {
//stdout is used by communication //stdout is used by communication
@ -179,14 +165,14 @@ func TestNewAcc(t *testing.T) {
verifyNum(4) verifyNum(4)
// Testing listing: // Testing listing:
// Listing one account // Listing one Account
control <- "1" control <- "1"
list, err := api.List(context.Background()) list, err := api.List(context.Background())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(list) != 1 { if len(list) != 1 {
t.Fatalf("List should only show one account") t.Fatalf("List should only show one Account")
} }
// Listing denied // Listing denied
control <- "Nope" control <- "Nope"
@ -333,3 +319,31 @@ func TestSignTx(t *testing.T) {
} }
} }
/*
func TestAsyncronousResponses(t *testing.T){
//Set up one account
api, control := setup(t)
createAccount(control, api, t)
// Two transactions, the second one with larger value than the first
tx1 := mkTestTx()
newVal := big.NewInt(0).Add((*big.Int) (tx1.Value), big.NewInt(1))
tx2 := mkTestTx()
tx2.Value = (*hexutil.Big)(newVal)
control <- "W" //wait
control <- "Y" //
control <- "apassword"
control <- "Y" //
control <- "apassword"
var err error
h1, err := api.SignTransaction(context.Background(), common.HexToAddress("1111"), tx1, nil)
h2, err := api.SignTransaction(context.Background(), common.HexToAddress("2222"), tx2, nil)
}
*/

View file

@ -56,13 +56,11 @@ func (ui *CommandlineUI) readString() string {
func (ui *CommandlineUI) readPassword() string { func (ui *CommandlineUI) readPassword() string {
fmt.Printf("Enter password to approve:\n") fmt.Printf("Enter password to approve:\n")
fmt.Printf("> ") fmt.Printf("> ")
//TODO; remove this, only for debuggging within IDE
text := "foobar" text, err := terminal.ReadPassword(int(os.Stdin.Fd()))
//TODO: Use this if err != nil {
// text, err := terminal.ReadPassword(int(os.Stdin.Fd())) log.Crit("Failed to read password", "err", err)
//if err != nil { }
// log.Crit("Failed to read password", "err", err)
//}
fmt.Println() fmt.Println()
fmt.Println("-----------------------") fmt.Println("-----------------------")
return string(text) return string(text)
@ -95,112 +93,110 @@ func showMetadata(metadata Metadata) {
fmt.Printf("Request info:\n\t%v -> %v -> %v\n", metadata.remote, metadata.scheme, metadata.local) fmt.Printf("Request info:\n\t%v -> %v -> %v\n", metadata.remote, metadata.scheme, metadata.local)
} }
// ApproveTx prompt the user for confirmation to request to sign transaction // ApproveTx prompt the user for confirmation to request to sign Transaction
func (ui *CommandlineUI) ApproveTx(request *SignTxRequest, metadata Metadata, ch chan SignTxResponse) { func (ui *CommandlineUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
ui.mu.Lock() ui.mu.Lock()
defer ui.mu.Unlock() defer ui.mu.Unlock()
weival := request.transaction.Value() weival := request.Transaction.Value
fmt.Printf("--------- Transaction request-------------\n") fmt.Printf("--------- Transaction request-------------\n")
fmt.Printf("to: %v\n", request.transaction.To().Hex()) fmt.Printf("to: %v\n", request.Transaction.To.Hex())
fmt.Printf("from: %v\n", request.from.Address.Hex()) fmt.Printf("from: %v\n", request.From.Hex())
fmt.Printf("value: %v wei\n", weival) fmt.Printf("value: %v wei\n", weival)
if len(request.transaction.Data()) > 0 { if len(request.Transaction.Data) > 0 {
fmt.Printf("data: %v\n", common.Bytes2Hex(request.transaction.Data())) fmt.Printf("data: %v\n", common.Bytes2Hex(request.Transaction.Data))
} }
if request.callinfo != nil { if request.Callinfo != nil {
fmt.Printf("\nNote: This transaction contains data. Review abi-decoding info below:") fmt.Printf("\nNote: This Transaction contains data. Review abi-decoding info below:")
fmt.Printf("\nCall info:\n\t%v\n", request.callinfo.String()) fmt.Printf("\nCall info:\n\t%v\n", request.Callinfo.String())
} }
fmt.Printf("\n") fmt.Printf("\n")
showMetadata(metadata) showMetadata(request.Meta)
fmt.Printf("-------------------------------------------\n") fmt.Printf("-------------------------------------------\n")
ch <- SignTxResponse{request.transaction, true, ui.readPassword()} return SignTxResponse{request.Transaction, request.From, true, ui.readPassword()}, nil
} }
// ApproveSignData prompt the user for confirmation to request to sign data // ApproveSignData prompt the user for confirmation to request to sign data
func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest, metadata Metadata, ch chan SignDataResponse) { func (ui *CommandlineUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
ui.mu.Lock() ui.mu.Lock()
defer ui.mu.Unlock() defer ui.mu.Unlock()
fmt.Printf("-------- Sign data request--------------\n") fmt.Printf("-------- Sign data request--------------\n")
fmt.Printf("account: %x\n", request.account.Address) fmt.Printf("Account: %x\n", request.Address)
fmt.Printf("message: \n%v\n", request.message) fmt.Printf("message: \n%v\n", request.Message)
fmt.Printf("raw data: \n%v\n", request.rawdata) fmt.Printf("raw data: \n%v\n", request.Rawdata)
fmt.Printf("message hash: %v\n", request.hash) fmt.Printf("message hash: %v\n", request.Hash)
fmt.Printf("-------------------------------------------\n") fmt.Printf("-------------------------------------------\n")
showMetadata(metadata) showMetadata(request.Meta)
ch <- SignDataResponse{true, ui.readPassword()} return SignDataResponse{true, ui.readPassword()}, nil
} }
// ApproveExport prompt the user for confirmation to export encrypted account json // ApproveExport prompt the user for confirmation to export encrypted Account json
func (ui *CommandlineUI) ApproveExport(request *ExportRequest, metadata Metadata, ch chan ExportResponse) { func (ui *CommandlineUI) ApproveExport(request *ExportRequest) (ExportResponse, error) {
ui.mu.Lock() ui.mu.Lock()
defer ui.mu.Unlock() defer ui.mu.Unlock()
fmt.Printf("-------- Export account request--------------\n") fmt.Printf("-------- Export Account request--------------\n")
fmt.Printf("A request has been made to export the (encrypted) keyfile\n") fmt.Printf("A request has been made to export the (encrypted) keyfile\n")
fmt.Printf("Approving this operation means that the caller obtains the (encrypted) contents\n") fmt.Printf("Approving this operation means that the caller obtains the (encrypted) contents\n")
fmt.Printf("\n") fmt.Printf("\n")
fmt.Printf("account: %x\n", request.account.Address) fmt.Printf("Account: %x\n", request.Address)
fmt.Printf("keyfile: \n%v\n", request.file) //fmt.Printf("keyfile: \n%v\n", request.file)
fmt.Printf("-------------------------------------------\n") fmt.Printf("-------------------------------------------\n")
showMetadata(metadata) showMetadata(request.Meta)
ch <- ExportResponse{ui.confirm()} return ExportResponse{ui.confirm()}, nil
} }
// ApproveImport prompt the user for confirmation to import account json // ApproveImport prompt the user for confirmation to import Account json
func (ui *CommandlineUI) ApproveImport(request *ImportRequest, metadata Metadata, ch chan ImportResponse) { func (ui *CommandlineUI) ApproveImport(request *ImportRequest) (ImportResponse, error) {
ui.mu.Lock() ui.mu.Lock()
defer ui.mu.Unlock() defer ui.mu.Unlock()
fmt.Printf("-------- Export account request--------------\n") fmt.Printf("-------- Export Account request--------------\n")
fmt.Printf("A request has been made to import an encrypted keyfile\n") fmt.Printf("A request has been made to import an encrypted keyfile\n")
fmt.Printf("-------------------------------------------\n") fmt.Printf("-------------------------------------------\n")
showMetadata(metadata) showMetadata(request.Meta)
if ui.confirm() { if !ui.confirm() {
ch <- ImportResponse{true, ui.readPasswordText("Old password"), ui.readPasswordText("New password")} return ImportResponse{false, "", ""}, nil
} else {
ch <- ImportResponse{false, "", ""}
} }
return ImportResponse{true, ui.readPasswordText("Old password"), ui.readPasswordText("New password")}, nil
} }
// ApproveListing prompt the user for confirmation to list accounts // ApproveListing prompt the user for confirmation to list accounts
// the list of accounts to list can be modified by the ui // the list of accounts to list can be modified by the ui
func (ui *CommandlineUI) ApproveListing(request *ListRequest, metadata Metadata, ch chan ListResponse) { func (ui *CommandlineUI) ApproveListing(request *ListRequest) (ListResponse, error) {
ui.mu.Lock() ui.mu.Lock()
defer ui.mu.Unlock() defer ui.mu.Unlock()
fmt.Printf("-------- List account request--------------\n") fmt.Printf("-------- List Account request--------------\n")
fmt.Printf("A request has been made to list all accounts. \n") fmt.Printf("A request has been made to list all accounts. \n")
fmt.Printf("You can select which accounts the caller can see\n") fmt.Printf("You can select which accounts the caller can see\n")
for _, account := range request.accounts { for _, account := range request.Accounts {
fmt.Printf("\t[x] %v\n", account.Address.Hex()) fmt.Printf("\t[x] %v\n", account.Address.Hex())
} }
fmt.Printf("-------------------------------------------\n") fmt.Printf("-------------------------------------------\n")
showMetadata(metadata) showMetadata(request.Meta)
if ui.confirm() { if !ui.confirm() {
ch <- ListResponse{request.accounts} return ListResponse{nil}, nil
} else {
ch <- ListResponse{nil}
} }
return ListResponse{request.Accounts}, nil
} }
// ApproveNewAccount prompt the user for confirmation to create new account, and reveal to caller // ApproveNewAccount prompt the user for confirmation to create new Account, and reveal to caller
func (ui *CommandlineUI) ApproveNewAccount(requst *NewAccountRequest, metadata Metadata, ch chan NewAccountResponse) { func (ui *CommandlineUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
ui.mu.Lock() ui.mu.Lock()
defer ui.mu.Unlock() defer ui.mu.Unlock()
fmt.Printf("-------- New account request--------------\n") fmt.Printf("-------- New Account request--------------\n")
fmt.Printf("A request has been made to create a new. \n") fmt.Printf("A request has been made to create a new. \n")
fmt.Printf("Approving this operation means that a new account is created,\n") fmt.Printf("Approving this operation means that a new Account is created,\n")
fmt.Printf("and the address show to the caller\n") fmt.Printf("and the address show to the caller\n")
showMetadata(metadata) showMetadata(request.Meta)
ch <- NewAccountResponse{ui.confirm(), ui.readPassword()} return NewAccountResponse{ui.confirm(), ui.readPassword()}, nil
} }
// ShowError displays error message to user // ShowError displays error message to user

View file

@ -30,13 +30,14 @@ import (
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"gopkg.in/urfave/cli.v1" "gopkg.in/urfave/cli.v1"
"io"
) )
func main() { func main() {
app := cli.NewApp() app := cli.NewApp()
app.Name = "signer" app.Name = "signer"
app.Usage = "Manage ethereum account operations" app.Usage = "Manage ethereum Account operations"
app.Flags = []cli.Flag{ app.Flags = []cli.Flag{
cli.Int64Flag{ cli.Int64Flag{
Name: "chainid", Name: "chainid",
@ -77,12 +78,33 @@ func main() {
Usage: "File containing requests to handle", Usage: "File containing requests to handle",
Value: "", Value: "",
}, },
cli.BoolFlag{
Name: "stdio-ui",
Usage: "Use STDIN/STDOUT as a channel for an external UI. " +
"This means that an STDIN/STDOUT is used for RPC-communication with a e.g. a graphical user " +
"interface, and can be used when the signer is started by an external process.",
},
} }
app.Action = func(c *cli.Context) error { app.Action = func(c *cli.Context) error {
// Set up the logger to print everything and the random generator
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int("loglevel")), log.StreamHandler(os.Stdout, log.TerminalFormat(true))))
var (
ui SignerUI
logOutput io.Writer
)
if c.Bool("stdio-ui") {
logOutput = os.Stderr
ui = NewStdIOUI()
} else {
ui = NewCommandlineUI()
logOutput = os.Stdout
}
// Set up the logger to print everything
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(c.Int("loglevel")), log.StreamHandler(logOutput, log.TerminalFormat(true))))
if c.Bool("stdio-ui") {
log.Info("Using stdin/stdout as UI-channel")
}
db, err := NewAbiDBFromFile(c.String("4bytedb")) db, err := NewAbiDBFromFile(c.String("4bytedb"))
if err != nil { if err != nil {
@ -97,17 +119,18 @@ func main() {
c.Int64(utils.NetworkIdFlag.Name), c.Int64(utils.NetworkIdFlag.Name),
c.String("keystore"), c.String("keystore"),
c.Bool(utils.NoUSBFlag.Name), c.Bool(utils.NoUSBFlag.Name),
NewCommandlineUI(), db, ui, db,
c.Bool(utils.LightKDFFlag.Name)) c.Bool(utils.LightKDFFlag.Name))
listener net.Listener listener net.Listener
) )
// Audit logging
if logfile := c.String("auditlog"); logfile != "" { if logfile := c.String("auditlog"); logfile != "" {
f, err := os.OpenFile(logfile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) f, err := os.OpenFile(logfile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil { if err != nil {
utils.Fatalf("Could not open %v for audit logging", logfile) utils.Fatalf("Could not open %v for audit logging", logfile)
} }
server.SetAuditLogger(NewAuditLogger(f)) server.SetAuditLogger(NewAuditLogger(f))
log.Info("Writing audit logs to %v", logfile) log.Info("Audit logs configured", "file", logfile)
} }
// register signer API with server // register signer API with server
if err = server.RegisterName("account", api); err != nil { if err = server.RegisterName("account", api); err != nil {
@ -125,24 +148,23 @@ func main() {
if listener, err = net.Listen("tcp", endpoint); err != nil { if listener, err = net.Listen("tcp", endpoint); err != nil {
utils.Fatalf("Could not start http listener: %v", err) utils.Fatalf("Could not start http listener: %v", err)
} }
log.Info(fmt.Sprintf("HTTP endpoint opened: http://%s", endpoint)) log.Info("HTTP endpoint opened", "url", fmt.Sprintf("http://%s", endpoint))
cors := []string{"*"} cors := []string{"*"}
rpc.NewHTTPServer(cors, server).Serve(listener) rpc.NewHTTPServer(cors, server).Serve(listener)
return nil return nil
} }
app.Run(os.Args) app.Run(os.Args)
} }
// Create account // Create Account
// curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_new","params":["test"],"id":67}' localhost:8550 // curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_new","params":["test"],"id":67}' localhost:8550
// List accounts // List accounts
// curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_list","params":[""],"id":67}' http://localhost:8550/ // curl -i -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"account_list","params":[""],"id":67}' http://localhost:8550/
// Make transaction // Make Transaction
// safeSend(0x12) // safeSend(0x12)
// 4401a6e40000000000000000000000000000000000000000000000000000000000000012 // 4401a6e40000000000000000000000000000000000000000000000000000000000000012

100
cmd/signer/pythonsigner.py Normal file
View file

@ -0,0 +1,100 @@
import os,sys, subprocess
from tinyrpc.transports import ServerTransport
from tinyrpc.protocols.jsonrpc import JSONRPCProtocol
from tinyrpc.dispatch import RPCDispatcher
from tinyrpc.server import RPCServer
""" This is a POC example of how to write a custom UI for the signer. The UI starts the
signer process with the '--stdio-ui' option, and communicates with the signer binary
using standard input / output.
The standard input/output is a relatively secure way to communicate, as it does not require opening any ports
or IPC files. Needless to say, it does not protect against memory inspection mechanisms where an attacker
can access process memory."""
try:
import urllib.parse as urlparse
except ImportError:
import urllib as urlparse
class StdIOTransport(ServerTransport):
""" Uses std input/output for RPC """
def receive_message(self):
return None, urlparse.unquote(sys.stdin.readline())
def send_reply(self, context, reply):
print(reply)
class PipeTransport(ServerTransport):
""" Uses std a pipe for RPC """
def __init__(self,input, output):
self.input = input
self.output = output
def receive_message(self):
data = self.input.readline()
print("IN ->\n{}".format( data))
return None, urlparse.unquote(data)
def send_reply(self, context, reply):
print("OUT <-\n{}".format( reply))
self.output.write(reply)
self.output.write("\n")
dispatcher = RPCDispatcher()
@dispatcher.public
def ApproveTx(Transaction = None, From = None, Callinfo = None, Meta = None):
return {
"Approved" : True,
"Transaction" : Transaction,
"From" : From,
"Password" : None,
}
@dispatcher.public
def ApproveSignData():
return {"Approved": False,
"Password" : None}
@dispatcher.public
def ApproveExport():
return {"Approved" : False}
@dispatcher.public
def ApproveImport():
return {"Approved" : False, "OldPassword": "", "NewPassword": ""}
@dispatcher.public
def ApproveListing():
return []
@dispatcher.public
def ApproveNewAccount():
return {"Approved": False, "Password": ""}
@dispatcher.public
def ShowError(text = ""):
sys.err.println("Error: %s", text)
return
@dispatcher.public
def ShowInfo(text = ""):
sys.err.println("Info: %s", text)
return
def main():
# line buffered
p = subprocess.Popen(["./signer", "--stdio-ui"], bufsize=1, universal_newlines=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
transport = PipeTransport(p.stdout, p.stdin)
rpc_server = RPCServer(
transport,
JSONRPCProtocol(),
dispatcher
)
rpc_server.serve_forever()
if __name__ == '__main__':
main()

128
cmd/signer/stdioui.go Normal file
View file

@ -0,0 +1,128 @@
// Copyright 2017 The go-ethereum Authors
// This file is part of go-ethereum.
//
// go-ethereum 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.
//
// go-ethereum 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 go-ethereum. If not, see <http://www.gnu.org/licenses/>.
//
package main
import (
"bufio"
"github.com/ethereum/go-ethereum/log"
"github.com/powerman/rpc-codec/jsonrpc2"
"io"
"os"
"sync"
)
type StdIOUI struct {
client *jsonrpc2.Client
// codec rpc.ClientCodec
mu sync.Mutex
}
func NewStdIOUI() *StdIOUI {
in, out := bufio.NewReader(os.Stdin), os.Stdout
//codec := rpc2.NewJSONCodec()
return &StdIOUI{client: jsonrpc2.NewClient(&rwc{in, out})}
//return &StdIOUI{}
}
func (ui StdIOUI) dispatch(serviceMethod string, args interface{}, reply interface{}) error {
// ui.mu.Lock()
// defer ui.mu.Unlock()
//This is not synchronized, which should not be necssary. Ideally, the UI should be able
// to get requests and send responses out-of-order -- thus the rpc has an ID.
// in, out := bufio.NewReader(os.Stdin), os.Stdout
// codec := jsonrpc.NewClientCodec(&rwc{in, out})
// c := rpc.NewClientWithCodec(codec)
// return c.Call(serviceMethod, args, &reply)
err := ui.client.Call(serviceMethod, args, &reply)
if err != nil {
log.Info("Error", "exc", err.Error())
}
return err
}
func (ui StdIOUI) ApproveTx(request *SignTxRequest) (SignTxResponse, error) {
result := SignTxResponse{}
if err := ui.dispatch("ApproveTx", request, &result); err != nil {
return result, err
}
return result, nil
}
func (ui StdIOUI) ApproveSignData(request *SignDataRequest) (SignDataResponse, error) {
var result SignDataResponse
if err := ui.dispatch("ApproveSignData", request, &result); err != nil {
return result, err
}
return result, nil
}
func (ui StdIOUI) ApproveExport(request *ExportRequest) (ExportResponse, error) {
var result ExportResponse
if err := ui.dispatch("ApproveExport", request, &result); err != nil {
return result, err
}
return result, nil
}
func (ui StdIOUI) ApproveImport(request *ImportRequest) (ImportResponse, error) {
var result ImportResponse
if err := ui.dispatch("ApproveImport", request, &result); err != nil {
return result, err
}
return result, nil
}
func (ui StdIOUI) ApproveListing(request *ListRequest) (ListResponse, error) {
var result ListResponse
if err := ui.dispatch("ApproveListing", request, &result); err != nil {
return result, err
}
return result, nil
}
func (ui StdIOUI) ApproveNewAccount(request *NewAccountRequest) (NewAccountResponse, error) {
var result NewAccountResponse
if err := ui.dispatch("ApproveNewAccount", request, &result); err != nil {
return result, err
}
return result, nil
}
func (ui StdIOUI) ShowError(message string) {
}
func (ui StdIOUI) ShowInfo(message string) {
}
type rwc struct {
io.Reader
io.Writer
}
func (r *rwc) Close() error {
if err := os.Stdin.Close(); err != nil {
return err
}
return os.Stdout.Close()
//return nil
}

View file

@ -48,7 +48,7 @@ func (a Account) String() string {
return err.Error() return err.Error()
} }
// TransactionArg represents a transaction for the signer. // TransactionArg represents a Transaction for the signer.
type TransactionArg struct { type TransactionArg struct {
To *common.Address `json:"to"` To *common.Address `json:"to"`
Gas *hexutil.Big `json:"gas"` Gas *hexutil.Big `json:"gas"`