diff --git a/cmd/server/main.go b/cmd/server/main.go index 0408a1d..ce0911a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -9,7 +9,6 @@ import ( "syscall" "time" - "github.com/btcsuite/btcd/rpcclient" "github.com/sirupsen/logrus" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -93,12 +92,12 @@ func main() { flag.StringVar(&opts.tlsKeyPath, "tls-key", "", "the path to a TLS key file (optional)") flag.Uint64Var(&opts.logLevel, "log-level", uint64(logrus.InfoLevel), "log level (logrus 1-7)") flag.StringVar(&opts.logPath, "log-file", "", "log file to write to") - flag.Stringvar(&opt.zcashConfPath, "conf-file", "~/.zcash/zcash.conf", "conf file to pull RPC creds from") + flag.StringVar(&opts.zcashConfPath, "conf-file", "", "conf file to pull RPC creds from") // TODO prod metrics // TODO support config from file and env vars flag.Parse() - if opts.dbPath == "" { + if opts.dbPath == "" || opts.zcashConfPath == "" { flag.Usage() os.Exit(1) } @@ -119,26 +118,6 @@ func main() { logger.SetLevel(logrus.Level(opts.logLevel)) - // Initialize Zcash RPC client. Right now (Jan 2018) this is only for - // sending transactions, but in the future it could back a different type - // of block streamer. - - var rpcClient *rpcclient.Client - rpcClient, err = NewZRPCFromConfig(opts.zcashConfPath) - if err != nil { - log.WithFields(logrus.Fields{ - "error": err, - }).Warn("zcash.conf failed, will try empty credentials for rpc") - - rpcClient, err = NewZRPCFromCreds(opts.bindAddr, "", "") - - if err != nil { - log.WithFields(logrus.Fields{ - "error": err, - }).Warn("couldn't start rpc conn. won't be able to send transactions") - } - } - // gRPC initialization var server *grpc.Server @@ -161,13 +140,32 @@ func main() { reflection.Register(server) } + // Initialize Zcash RPC client. Right now (Jan 2018) this is only for + // sending transactions, but in the future it could back a different type + // of block streamer. + + rpcClient, err := frontend.NewZRPCFromConf(opts.zcashConfPath) + if err != nil { + log.WithFields(logrus.Fields{ + "error": err, + }).Warn("zcash.conf failed, will try empty credentials for rpc") + + rpcClient, err = frontend.NewZRPCFromCreds("127.0.0.1:8232", "", "") + + if err != nil { + log.WithFields(logrus.Fields{ + "error": err, + }).Warn("couldn't start rpc conn. won't be able to send transactions") + } + } + // Compact transaction service initialization - service, err := frontend.NewSQLiteStreamer(opts.dbPath) + service, err := frontend.NewSQLiteStreamer(opts.dbPath, rpcClient) if err != nil { log.WithFields(logrus.Fields{ "db_path": opts.dbPath, "error": err, - }).Fatal("couldn't create SQL streamer") + }).Fatal("couldn't create SQL backend") } defer service.(*frontend.SqlStreamer).GracefulStop() diff --git a/frontend/rpc_client.go b/frontend/rpc_client.go index e3cc518..bd03ff6 100644 --- a/frontend/rpc_client.go +++ b/frontend/rpc_client.go @@ -1,16 +1,25 @@ -// +build ignore - package frontend import ( - "log" + "net" "github.com/btcsuite/btcd/rpcclient" + "github.com/pkg/errors" + ini "gopkg.in/ini.v1" ) func NewZRPCFromConf(confPath string) (*rpcclient.Client, error) { - return nil, errors.New("not yet implemented") - //return NewZRPCFromCreds(addr, username, password) + cfg, err := ini.Load(confPath) + if err != nil { + return nil, errors.Wrap(err, "failed to read config file") + } + + rpcaddr := cfg.Section("").Key("rpcbind").String() + rpcport := cfg.Section("").Key("rpcport").String() + username := cfg.Section("").Key("rpcuser").String() + password := cfg.Section("").Key("rpcpassword").String() + + return NewZRPCFromCreds(net.JoinHostPort(rpcaddr, rpcport), username, password) } func NewZRPCFromCreds(addr, username, password string) (*rpcclient.Client, error) { diff --git a/frontend/rpc_test.go b/frontend/rpc_test.go new file mode 100644 index 0000000..9ab2776 --- /dev/null +++ b/frontend/rpc_test.go @@ -0,0 +1,39 @@ +package frontend + +import ( + "encoding/json" + "strconv" + "strings" + "testing" +) + +// a well-formed raw transaction +const coinbaseTxHex = "0400008085202f89010000000000000000000000000000000000000" + + "000000000000000000000000000ffffffff03580101ffffffff0200ca9a3b000000001976a9146b" + + "9ae8c14e917966b0afdf422d32dbac40486d3988ac80b2e60e0000000017a9146708e6670db0b95" + + "0dac68031025cc5b63213a4918700000000000000000000000000000000000000" + +func TestSendTransaction(t *testing.T) { + client, err := NewZRPCFromCreds("127.0.0.1:8232", "user", "password") + if err != nil { + t.Fatalf("Couldn't init JSON-RPC client: %v", err) + } + + params := make([]json.RawMessage, 1) + params[0] = json.RawMessage("\"" + coinbaseTxHex + "\"") + _, err = client.RawRequest("sendrawtransaction", params) + if err == nil { + t.Fatal("somehow succeeded at sending a coinbase tx") + } + + errParts := strings.SplitN(err.Error(), ":", 2) + errCode, err := strconv.ParseInt(errParts[0], 10, 64) + if err != nil { + t.Errorf("couldn't parse error code: %v", err) + } + errMsg := strings.TrimSpace(errParts[1]) + + if errCode != -26 || errMsg != "16: coinbase" { + t.Error("got the wrong errors") + } +} diff --git a/frontend/service.go b/frontend/service.go index 19a7c5a..9c779be 100644 --- a/frontend/service.go +++ b/frontend/service.go @@ -4,11 +4,13 @@ import ( "context" "database/sql" "encoding/hex" + "encoding/json" "errors" "fmt" + "strconv" + "strings" "time" - "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/rpcclient" "github.com/golang/protobuf/proto" @@ -20,7 +22,6 @@ import ( ) var ( - ErrNoImpl = errors.New("not yet implemented") ErrUnspecified = errors.New("request for unspecified identifier") ) @@ -30,7 +31,7 @@ type SqlStreamer struct { client *rpcclient.Client } -func NewSQLiteStreamer(dbPath string) (walletrpc.CompactTxStreamerServer, error) { +func NewSQLiteStreamer(dbPath string, client *rpcclient.Client) (walletrpc.CompactTxStreamerServer, error) { db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?_busy_timeout=10000&cache=shared", dbPath)) db.SetMaxOpenConns(1) if err != nil { @@ -43,7 +44,7 @@ func NewSQLiteStreamer(dbPath string) (walletrpc.CompactTxStreamerServer, error) return nil, err } - return &SqlStreamer{db}, nil + return &SqlStreamer{db, client}, nil } func (s *SqlStreamer) GracefulStop() error { @@ -155,15 +156,46 @@ func (s *SqlStreamer) GetTransaction(ctx context.Context, txf *walletrpc.TxFilte // SendTransaction forwards raw transaction bytes to a zcashd instance over JSON-RPC func (s *SqlStreamer) SendTransaction(ctx context.Context, rawtx *walletrpc.RawTransaction) (*walletrpc.SendResponse, error) { // sendrawtransaction "hexstring" ( allowhighfees ) - txHexString := hex.EncodeToSring(rawtx.Data) - cmd := btcjson.NewSendRawTransactionCmd(txHexString, false) - result, err := s.client.sendCmd(cmd).Receive() + // + // Submits raw transaction (serialized, hex-encoded) to local node and network. + // + // Also see createrawtransaction and signrawtransaction calls. + // + // Arguments: + // 1. "hexstring" (string, required) The hex string of the raw transaction) + // 2. allowhighfees (boolean, optional, default=false) Allow high fees + // + // Result: + // "hex" (string) The transaction hash in hex + + // Construct raw JSON-RPC params + params := make([]json.RawMessage, 1) + txHexString := hex.EncodeToString(rawtx.Data) + params[0] = json.RawMessage("\"" + txHexString + "\"") + result, rpcErr := s.client.RawRequest("sendrawtransaction", params) - // TODO figure out this error handling. - // zcash-cli gets a signed number and a message + var err error + var errCode int64 + var errMsg string + + // For some reason, the error responses are not JSON + if rpcErr != nil { + errParts := strings.SplitN(rpcErr.Error(), ":", 2) + errMsg = strings.TrimSpace(errParts[1]) + errCode, err = strconv.ParseInt(errParts[0], 10, 32) + if err != nil { + // This should never happen. We can't panic here, but it's that class of error. + // This is why we need integration testing to work better than regtest currently does. TODO. + return nil, errors.New("SendTransaction couldn't parse error code") + } + } else { + errMsg = string(result) + } + // TODO these are called Error but they aren't at the moment. + // A success will return code 0 and message txhash. return &walletrpc.SendResponse{ - //ErrorCode: err, - ErrorMessage: result, - }, err + ErrorCode: int32(errCode), + ErrorMessage: errMsg, + }, nil } diff --git a/go.mod b/go.mod index d8134e3..83449a6 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,14 @@ go 1.12 require ( github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d github.com/golang/protobuf v1.2.0 + github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/mattn/go-sqlite3 v1.10.0 github.com/pebbe/zmq4 v1.0.0 github.com/pkg/errors v0.8.0 github.com/sirupsen/logrus v1.2.0 + github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect + github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect golang.org/x/net v0.0.0-20181220203305-927f97764cc3 google.golang.org/grpc v1.17.0 + gopkg.in/ini.v1 v1.41.0 ) diff --git a/go.sum b/go.sum index 49e7500..caf5dbe 100644 --- a/go.sum +++ b/go.sum @@ -20,12 +20,15 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= @@ -43,6 +46,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= +github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -54,6 +61,7 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -72,6 +80,8 @@ google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.41.0 h1:Ka3ViY6gNYSKiVy71zXBEqKplnV35ImDLVG+8uoIklE= +gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=