diff --git a/rpc.go b/rpc.go index a8c350b..bfe5f65 100644 --- a/rpc.go +++ b/rpc.go @@ -17,14 +17,41 @@ package main import ( + "encoding/json" "github.com/conformal/btcjson" ) -// RPCResponse is an interface type covering both server -// (frontend <-> btcwallet) and client (btcwallet <-> btcd) responses. -type RPCResponse interface { - Result() interface{} - Error() *btcjson.Error +// RawRPCResponse is a response to a JSON-RPC request with delayed +// unmarshaling. +type RawRPCResponse struct { + Id *uint64 + Result *json.RawMessage `json:"result"` + Error *json.RawMessage `json:"error"` +} + +func (r *RawRPCResponse) FinishUnmarshal(v interface{}) (interface{}, *btcjson.Error) { + // JSON-RPC spec makes this handling easier-ish because both result and + // error cannot be non-nil. + var jsonErr *btcjson.Error + if r.Error != nil { + if err := json.Unmarshal([]byte(*r.Error), &jsonErr); err != nil { + return nil, &btcjson.Error{ + Code: btcjson.ErrParse.Code, + Message: err.Error(), + } + } + return nil, jsonErr + } + if r.Result != nil { + if err := json.Unmarshal([]byte(*r.Result), &v); err != nil { + return nil, &btcjson.Error{ + Code: btcjson.ErrParse.Code, + Message: err.Error(), + } + } + return v, nil + } + return nil, nil } // ClientRequest is a type holding a bitcoin client's request and @@ -32,7 +59,7 @@ type RPCResponse interface { type ClientRequest struct { ws bool request btcjson.Cmd - response chan RPCResponse + response chan RawRPCResponse } // NewClientRequest creates a new ClientRequest from a btcjson.Cmd. @@ -40,78 +67,28 @@ func NewClientRequest(request btcjson.Cmd, ws bool) *ClientRequest { return &ClientRequest{ ws: ws, request: request, - response: make(chan RPCResponse), + response: make(chan RawRPCResponse), } } // Handle sends a client request to the RPC gateway for processing, // and returns the result when handling is finished. -func (r *ClientRequest) Handle() (interface{}, *btcjson.Error) { +func (r *ClientRequest) Handle() RawRPCResponse { clientRequests <- r - resp := <-r.response - return resp.Result(), resp.Error() -} - -// ClientResponse holds a result and error returned from handling a -// client's request. -type ClientResponse struct { - result interface{} - err *btcjson.Error -} - -// Result returns the result of a response to a client. -func (r *ClientResponse) Result() interface{} { - return r.result -} - -// Error returns the error of a response to a client, or nil if -// there is no error. -func (r *ClientResponse) Error() *btcjson.Error { - return r.err + return <-r.response } // ServerRequest is a type responsible for handling requests to a bitcoin // server and providing a method to access the response. type ServerRequest struct { request btcjson.Cmd - result interface{} - response chan RPCResponse + response chan RawRPCResponse } -// NewServerRequest creates a new ServerRequest from a btcjson.Cmd. request -// may be nil to create a new var for the result (with types determined by -// the unmarshaling rules described in the json package), or set to a var -// with an expected type (i.e. *btcjson.BlockResult) to directly unmarshal -// the response's result into a convenient type. -func NewServerRequest(request btcjson.Cmd, result interface{}) *ServerRequest { +// NewServerRequest creates a new ServerRequest from a btcjson.Cmd. +func NewServerRequest(request btcjson.Cmd) *ServerRequest { return &ServerRequest{ request: request, - result: result, - response: make(chan RPCResponse, 1), + response: make(chan RawRPCResponse, 1), } } - -// ServerResponse holds a response's result and error returned from sending a -// ServerRequest. -type ServerResponse struct { - // Result will be set to a concrete type (i.e. *btcjson.BlockResult) - // and may be type asserted to that type if a non-nil result was used - // to create the originating ServerRequest. Otherwise, Result will be - // set to new memory allocated by json.Unmarshal, and the type rules - // for unmarshaling described in the json package should be followed - // when type asserting Result. - result interface{} - - // Err points to an unmarshaled error, or nil if result is valid. - err *btcjson.Error -} - -// Result returns the result of a server's RPC response. -func (r *ServerResponse) Result() interface{} { - return r.result -} - -// Result returns the error of a server's RPC response. -func (r *ServerResponse) Error() *btcjson.Error { - return r.err -} diff --git a/rpcclient.go b/rpcclient.go index e4f1944..eae9327 100644 --- a/rpcclient.go +++ b/rpcclient.go @@ -37,7 +37,7 @@ type ServerConn interface { // SendRequest sends a bitcoin RPC request, returning a channel to // read the reply. A channel is used so both synchronous and // asynchronous RPC can be supported. - SendRequest(request *ServerRequest) chan RPCResponse + SendRequest(request *ServerRequest) chan RawRPCResponse } // ErrBtcdDisconnected describes an error where an operation cannot @@ -48,6 +48,9 @@ var ErrBtcdDisconnected = btcjson.Error{ Message: "btcd disconnected", } +// ErrBtcdDisconnectedRaw is the raw JSON encoding of ErrBtcdDisconnected. +var ErrBtcdDisconnectedRaw = json.RawMessage(`{"code":-1,"message":"btcd disconnected"}`) + // BtcdRPCConn is a type managing a client connection to a btcd RPC server // over websockets. type BtcdRPCConn struct { @@ -72,23 +75,20 @@ func NewBtcdRPCConn(ws *websocket.Conn) *BtcdRPCConn { // SendRequest sends an RPC request and returns a channel to read the response's // result and error. Part of the RPCConn interface. -func (btcd *BtcdRPCConn) SendRequest(request *ServerRequest) chan RPCResponse { +func (btcd *BtcdRPCConn) SendRequest(request *ServerRequest) chan RawRPCResponse { select { case <-btcd.closed: // The connection has closed, so instead of adding and sending // a request, return a channel that just replies with the // error for a disconnected btcd. - responseChan := make(chan RPCResponse, 1) - response := &ServerResponse{ - err: &ErrBtcdDisconnected, - } - responseChan <- response + responseChan := make(chan RawRPCResponse, 1) + responseChan <- RawRPCResponse{Error: &ErrBtcdDisconnectedRaw} return responseChan default: addRequest := &AddRPCRequest{ Request: request, - ResponseChan: make(chan chan RPCResponse, 1), + ResponseChan: make(chan chan RawRPCResponse, 1), } btcd.addRequest <- addRequest return <-addRequest.ResponseChan @@ -124,7 +124,7 @@ func (btcd *BtcdRPCConn) Close() { // being manaaged by a btcd RPC connection. type AddRPCRequest struct { Request *ServerRequest - ResponseChan chan chan RPCResponse + ResponseChan chan chan RawRPCResponse } // send performs the actual send of the marshaled request over the btcd @@ -136,17 +136,11 @@ func (btcd *BtcdRPCConn) send(rpcrequest *ServerRequest) error { return websocket.Message.Send(btcd.ws, mrequest) } -type receivedResponse struct { - id uint64 - raw string - reply *btcjson.Reply -} - // Start starts the goroutines required to send RPC requests and listen for // replies. func (btcd *BtcdRPCConn) Start() { done := btcd.closed - responses := make(chan *receivedResponse) + responses := make(chan RawRPCResponse) // Maintain a map of JSON IDs to RPCRequests currently being waited on. go func() { @@ -167,49 +161,61 @@ func (btcd *BtcdRPCConn) Start() { addrequest.ResponseChan <- rpcrequest.response - case recvResponse, ok := <-responses: + case rawResponse, ok := <-responses: if !ok { responses = nil close(done) break } - rpcrequest, ok := m[recvResponse.id] + rpcrequest, ok := m[*rawResponse.Id] if !ok { log.Warnf("Received unexpected btcd response") continue } - delete(m, recvResponse.id) + delete(m, *rawResponse.Id) - // If no result var was set, create and send - // send the response unmarshaled by the json - // package. - if rpcrequest.result == nil { + rpcrequest.response <- rawResponse + + /* + //rpcrequest.result + var jsonErr *btcjson.Error + if rawResponse.result != nil { + } + err := json.Unmarshal([]byte(*rawResponse.result), &result) + err := json.Unmarshal([]byte(*rawResponse.error), &error) + + rawResult := recvResponse + rawError := recvResponse response := &ServerResponse{ - result: recvResponse.reply.Result, - err: recvResponse.reply.Error, + result: r.Result, + err: jsonErr } rpcrequest.response <- response - continue - } + */ - // A return var was set, so unmarshal again - // into the var before sending the response. - r := &btcjson.Reply{ - Result: rpcrequest.result, - } - json.Unmarshal([]byte(recvResponse.raw), &r) - response := &ServerResponse{ - result: r.Result, - err: r.Error, - } - rpcrequest.response <- response + /* + // If no result var was set, create and send + // send the response unmarshaled by the json + // package. + + + if rpcrequest.result == nil { + response := &ServerResponse{ + result: recvResponse.reply.Result, + err: recvResponse.reply.Error, + } + rpcrequest.response <- response + continue + } + + // A return var was set, so unmarshal again + // into the var before sending the response. + */ case <-done: - response := &ServerResponse{ - err: &ErrBtcdDisconnected, - } + resp := RawRPCResponse{Error: &ErrBtcdDisconnectedRaw} for _, request := range m { - request.response <- response + request.response <- resp } return } @@ -256,28 +262,18 @@ func (btcd *BtcdRPCConn) Start() { }() } -// unmarshalResponse attempts to unmarshal a marshaled JSON-RPC -// response. -func unmarshalResponse(s string) (*receivedResponse, error) { - var r btcjson.Reply +// unmarshalResponse attempts to unmarshal a marshaled JSON-RPC response. +func unmarshalResponse(s string) (RawRPCResponse, error) { + var r RawRPCResponse if err := json.Unmarshal([]byte(s), &r); err != nil { - return nil, err + return r, err } // Check for a valid ID. if r.Id == nil { - return nil, errors.New("id is nil") + return r, errors.New("id is null") } - fid, ok := (*r.Id).(float64) - if !ok { - return nil, errors.New("id is not a number") - } - response := &receivedResponse{ - id: uint64(fid), - raw: s, - reply: &r, - } - return response, nil + return r, nil } // unmarshalNotification attempts to unmarshal a marshaled JSON-RPC @@ -307,61 +303,67 @@ type GetBestBlockResult struct { // in the main chain. func GetBestBlock(rpc ServerConn) (*GetBestBlockResult, *btcjson.Error) { cmd := btcws.NewGetBestBlockCmd(<-NewJSONID) - request := NewServerRequest(cmd, new(GetBestBlockResult)) - response := <-rpc.SendRequest(request) - if response.Error() != nil { - return nil, response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + + var resultData GetBestBlockResult + _, jsonErr := response.FinishUnmarshal(&resultData) + if jsonErr != nil { + return nil, jsonErr } - return response.Result().(*GetBestBlockResult), nil + return &resultData, nil } // GetBlock requests details about a block with the given hash. func GetBlock(rpc ServerConn, blockHash string) (*btcjson.BlockResult, *btcjson.Error) { // NewGetBlockCmd cannot fail with no optargs, so omit the check. cmd, _ := btcjson.NewGetBlockCmd(<-NewJSONID, blockHash) - request := NewServerRequest(cmd, new(btcjson.BlockResult)) - response := <-rpc.SendRequest(request) - if response.Error() != nil { - return nil, response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + + var resultData btcjson.BlockResult + _, jsonErr := response.FinishUnmarshal(&resultData) + if jsonErr != nil { + return nil, jsonErr } - return response.Result().(*btcjson.BlockResult), nil + return &resultData, nil } // GetCurrentNet requests the network a bitcoin RPC server is running on. func GetCurrentNet(rpc ServerConn) (btcwire.BitcoinNet, *btcjson.Error) { cmd := btcws.NewGetCurrentNetCmd(<-NewJSONID) - request := NewServerRequest(cmd, nil) - response := <-rpc.SendRequest(request) - if response.Error() != nil { - return 0, response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + + var resultData uint32 + _, jsonErr := response.FinishUnmarshal(&resultData) + if jsonErr != nil { + return 0, jsonErr } - return btcwire.BitcoinNet(uint32(response.Result().(float64))), nil + return btcwire.BitcoinNet(resultData), nil } // NotifyBlocks requests blockconnected and blockdisconnected notifications. func NotifyBlocks(rpc ServerConn) *btcjson.Error { cmd := btcws.NewNotifyBlocksCmd(<-NewJSONID) - request := NewServerRequest(cmd, nil) - response := <-rpc.SendRequest(request) - return response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + _, jsonErr := response.FinishUnmarshal(nil) + return jsonErr } // NotifyNewTXs requests notifications for new transactions that spend // to any of the addresses in addrs. func NotifyNewTXs(rpc ServerConn, addrs []string) *btcjson.Error { cmd := btcws.NewNotifyNewTXsCmd(<-NewJSONID, addrs) - request := NewServerRequest(cmd, nil) - response := <-rpc.SendRequest(request) - return response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + _, jsonErr := response.FinishUnmarshal(nil) + return jsonErr } // NotifySpent requests notifications for when a transaction is processed which // spends op. func NotifySpent(rpc ServerConn, op *btcwire.OutPoint) *btcjson.Error { cmd := btcws.NewNotifySpentCmd(<-NewJSONID, op) - request := NewServerRequest(cmd, nil) - response := <-rpc.SendRequest(request) - return response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + _, jsonErr := response.FinishUnmarshal(nil) + return jsonErr } // Rescan requests a blockchain rescan for transactions to any number of @@ -371,21 +373,23 @@ func Rescan(rpc ServerConn, beginBlock int32, addrs []string, // NewRescanCmd cannot fail with no optargs, so omit the check. cmd, _ := btcws.NewRescanCmd(<-NewJSONID, beginBlock, addrs, outpoints) - request := NewServerRequest(cmd, nil) - response := <-rpc.SendRequest(request) - return response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + _, jsonErr := response.FinishUnmarshal(nil) + return jsonErr } // SendRawTransaction sends a hex-encoded transaction for relay. func SendRawTransaction(rpc ServerConn, hextx string) (txid string, error *btcjson.Error) { // NewSendRawTransactionCmd cannot fail, so omit the check. cmd, _ := btcjson.NewSendRawTransactionCmd(<-NewJSONID, hextx) - request := NewServerRequest(cmd, new(string)) - response := <-rpc.SendRequest(request) - if response.Error() != nil { - return "", response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + + var resultData string + _, jsonErr := response.FinishUnmarshal(&resultData) + if jsonErr != nil { + return "", jsonErr } - return *response.Result().(*string), nil + return resultData, nil } // GetRawTransaction sends the non-verbose version of a getrawtransaction @@ -394,13 +398,14 @@ func SendRawTransaction(rpc ServerConn, hextx string) (txid string, error *btcjs func GetRawTransaction(rpc ServerConn, txsha *btcwire.ShaHash) (*btcutil.Tx, *btcjson.Error) { // NewGetRawTransactionCmd cannot fail with no optargs. cmd, _ := btcjson.NewGetRawTransactionCmd(<-NewJSONID, txsha.String()) - request := NewServerRequest(cmd, new(string)) - response := <-rpc.SendRequest(request) - if response.Error() != nil { - return nil, response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + + var resultData string + _, jsonErr := response.FinishUnmarshal(&resultData) + if jsonErr != nil { + return nil, jsonErr } - hextx := *response.Result().(*string) - serializedTx, err := hex.DecodeString(hextx) + serializedTx, err := hex.DecodeString(resultData) if err != nil { return nil, &btcjson.ErrDecodeHexString } @@ -416,10 +421,12 @@ func GetRawTransaction(rpc ServerConn, txsha *btcwire.ShaHash) (*btcutil.Tx, *bt func VerboseGetRawTransaction(rpc ServerConn, txsha *btcwire.ShaHash) (*btcjson.TxRawResult, *btcjson.Error) { // NewGetRawTransactionCmd cannot fail with a single optarg. cmd, _ := btcjson.NewGetRawTransactionCmd(<-NewJSONID, txsha.String(), 1) - request := NewServerRequest(cmd, new(btcjson.TxRawResult)) - response := <-rpc.SendRequest(request) - if response.Error() != nil { - return nil, response.Error() + response := <-rpc.SendRequest(NewServerRequest(cmd)) + + var resultData btcjson.TxRawResult + _, jsonErr := response.FinishUnmarshal(&resultData) + if jsonErr != nil { + return nil, jsonErr } - return response.Result().(*btcjson.TxRawResult), nil + return &resultData, nil } diff --git a/rpcserver.go b/rpcserver.go index 306a75c..aa44071 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -20,6 +20,7 @@ import ( "bytes" "encoding/base64" "encoding/hex" + "encoding/json" "github.com/conformal/btcec" "github.com/conformal/btcjson" "github.com/conformal/btcscript" @@ -118,6 +119,9 @@ var ErrServerBusy = btcjson.Error{ Message: "Server busy", } +// ErrServerBusyRaw is the raw JSON encoding of ErrServerBusy. +var ErrServerBusyRaw = json.RawMessage(`{"code":-32000,"message":"Server busy"}`) + // RPCGateway is the common entry point for all client RPC requests and // server notifications. If a request needs to be handled by btcwallet, // it is sent to WalletRequestProcessor's request queue, or dropped if the @@ -149,16 +153,15 @@ func RPCGateway() { case requestQueue <- r: default: // Server busy with too many requests. - resp := ClientResponse{ - err: &ErrServerBusy, + resp := RawRPCResponse{ + Error: &ErrServerBusyRaw, } - r.response <- &resp + r.response <- resp } } else { r.request.SetId(<-NewJSONID) request := &ServerRequest{ request: r.request, - result: nil, response: r.response, } CurrentServerConn().SendRequest(request) @@ -197,9 +200,16 @@ func WalletRequestProcessor() { result, jsonErr := f(r.request) AcctMgr.Release() - r.response <- &ClientResponse{ - result: result, - err: jsonErr, + if jsonErr != nil { + b, _ := json.Marshal(jsonErr) + r.response <- RawRPCResponse{ + Error: (*json.RawMessage)(&b), + } + } else { + b, _ := json.Marshal(result) + r.response <- RawRPCResponse{ + Result: (*json.RawMessage)(&b), + } } case n := <-handleNtfn: @@ -563,34 +573,35 @@ func GetInfo(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Call down to btcd for all of the information in this command known // by them. This call can not realistically ever fail. gicmd, _ := btcjson.NewGetInfoCmd(<-NewJSONID) - req := NewServerRequest(gicmd, make(map[string]interface{})) - response := <-CurrentServerConn().SendRequest(req) - if response.Error() != nil { - return nil, response.Error() + response := <-CurrentServerConn().SendRequest(NewServerRequest(gicmd)) + + var info btcjson.InfoResult + _, jsonErr := response.FinishUnmarshal(&info) + if jsonErr != nil { + return nil, jsonErr } - ret := response.Result().(map[string]interface{}) balance := float64(0.0) accounts := AcctMgr.ListAccounts(1) for _, v := range accounts { balance += v } - ret["walletversion"] = wallet.VersCurrent.Uint32() - ret["balance"] = balance + info.WalletVersion = int(wallet.VersCurrent.Uint32()) + info.Balance = balance // Keypool times are not tracked. set to current time. - ret["keypoololdest"] = time.Now().Unix() - ret["keypoolsize"] = cfg.KeypoolSize + info.KeypoolOldest = time.Now().Unix() + info.KeypoolSize = int(cfg.KeypoolSize) TxFeeIncrement.Lock() - ret["paytxfee"] = TxFeeIncrement.i + info.PaytxFee = float64(TxFeeIncrement.i) / float64(btcutil.SatoshiPerBitcoin) TxFeeIncrement.Unlock() /* * We don't set the following since they don't make much sense in the * wallet architecture: - * ret["unlocked_until"] - * ret["errors"] + * - unlocked_until + * - errors */ - return ret, nil + return info, nil } // GetAccount handles a getaccount request by returning the account name @@ -1070,8 +1081,7 @@ func ListSinceBlock(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { } } - req := NewServerRequest(gbh, new(string)) - bhChan := CurrentServerConn().SendRequest(req) + bhChan := CurrentServerConn().SendRequest(NewServerRequest(gbh)) txInfoList, err := AcctMgr.ListSinceBlock(height, bs.Height, cmd.TargetConfirmations) @@ -1084,15 +1094,15 @@ func ListSinceBlock(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { // Done with work, get the response. response := <-bhChan - if response.Error() != nil { - return nil, response.Error() + var hash string + _, jsonErr := response.FinishUnmarshal(&hash) + if jsonErr != nil { + return nil, jsonErr } - hash := response.Result().(*string) - res := make(map[string]interface{}) res["transactions"] = txInfoList - res["lastblock"] = *hash + res["lastblock"] = hash return res, nil } diff --git a/sockets.go b/sockets.go index d4169bc..5a96055 100644 --- a/sockets.go +++ b/sockets.go @@ -263,17 +263,23 @@ func (s *server) ReplyToFrontend(msg []byte, ws, authenticated bool) ([]byte, er } cReq := NewClientRequest(cmd, ws) - result, jsonErr := cReq.Handle() + rawResp := cReq.Handle() - response := btcjson.Reply{ - Id: &id, - Result: result, - Error: jsonErr, + response := struct { + Jsonrpc string `json:"jsonrpc"` + Id interface{} `json:"id"` + Result *json.RawMessage `json:"result"` + Error *json.RawMessage `json:"error"` + }{ + Jsonrpc: "1.0", + Id: id, + Result: rawResp.Result, + Error: rawResp.Error, } mresponse, err := json.Marshal(response) if err != nil { log.Errorf("Cannot marhal response: %v", err) - response = btcjson.Reply{ + response := btcjson.Reply{ Id: &id, Error: &btcjson.ErrInternal, }