/* * Copyright (c) 2013, 2014 Conformal Systems LLC * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ // This file implements the websocket client connection to a bitcoin RPC // server. package main import ( "code.google.com/p/go.net/websocket" "encoding/json" "errors" "github.com/conformal/btcjson" "github.com/conformal/btcwire" "github.com/conformal/btcws" ) // ServerConn is an interface representing a client connection to a bitcoin RPC // server. 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 } // ErrBtcdDisconnected describes an error where an operation cannot // successfully complete due to btcwallet not being connected to // btcd. var ErrBtcdDisconnected = btcjson.Error{ Code: -1, Message: "btcd disconnected", } // BtcdRPCConn is a type managing a client connection to a btcd RPC server // over websockets. type BtcdRPCConn struct { ws *websocket.Conn addRequest chan *AddRPCRequest closed chan struct{} } // Ensure that BtcdRPCConn can be used as an RPCConn. var _ ServerConn = &BtcdRPCConn{} // NewBtcdRPCConn creates a new RPC connection from a btcd websocket // connection to btcd. func NewBtcdRPCConn(ws *websocket.Conn) *BtcdRPCConn { conn := &BtcdRPCConn{ ws: ws, addRequest: make(chan *AddRPCRequest), closed: make(chan struct{}), } return conn } // 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 { 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 return responseChan default: addRequest := &AddRPCRequest{ Request: request, ResponseChan: make(chan chan RPCResponse, 1), } btcd.addRequest <- addRequest return <-addRequest.ResponseChan } } // Connected returns whether the connection remains established to the RPC // server. // // This function probably should be removed, as any checks for confirming // the connection are no longer valid after the check and may result in // races. func (btcd *BtcdRPCConn) Connected() bool { select { case <-btcd.closed: return false default: return true } } // AddRPCRequest is used to add an RPCRequest to the pool of requests // being manaaged by a btcd RPC connection. type AddRPCRequest struct { Request *ServerRequest ResponseChan chan chan RPCResponse } // send performs the actual send of the marshaled request over the btcd // websocket connection. func (btcd *BtcdRPCConn) send(rpcrequest *ServerRequest) error { // btcjson.Cmds define their own MarshalJSON which returns an error // to satisify the json.Marshaler interface, but will never error. mrequest, _ := rpcrequest.request.MarshalJSON() 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) // Maintain a map of JSON IDs to RPCRequests currently being waited on. go func() { m := make(map[uint64]*ServerRequest) for { select { case addrequest := <-btcd.addRequest: rpcrequest := addrequest.Request m[rpcrequest.request.Id().(uint64)] = rpcrequest if err := btcd.send(rpcrequest); err != nil { // Connection lost. btcd.ws.Close() close(done) } addrequest.ResponseChan <- rpcrequest.response case recvResponse := <-responses: rpcrequest, ok := m[recvResponse.id] if !ok { log.Warnf("Received unexpected btcd response") continue } delete(m, recvResponse.id) // 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. r := &btcjson.Reply{ Result: rpcrequest.result, } json.Unmarshal([]byte(recvResponse.raw), &r) response := &ServerResponse{ result: r.Result, err: r.Error, } rpcrequest.response <- response case <-done: for _, request := range m { response := &ServerResponse{ err: &ErrBtcdDisconnected, } request.response <- response } return } } }() // Listen for replies/notifications from btcd, and decide how to handle them. go func() { // Idea: instead of reading btcd messages from just one websocket // connection, maybe use two so the same connection isn't used // for both notifications and responses? Should make handling // must faster as unnecessary unmarshal attempts could be avoided. for { var m string if err := websocket.Message.Receive(btcd.ws, &m); err != nil { log.Debugf("Cannot receive btcd message: %v", err) close(done) return } // Try notifications (requests with nil ids) first. n, err := unmarshalNotification(m) if err == nil { svrNtfns <- n continue } // Must be a response. r, err := unmarshalResponse(m) if err == nil { responses <- r continue } // Not sure what was received but it isn't correct. log.Warnf("Received invalid message from btcd") } }() } // unmarshalResponse attempts to unmarshal a marshaled JSON-RPC // response. func unmarshalResponse(s string) (*receivedResponse, error) { var r btcjson.Reply if err := json.Unmarshal([]byte(s), &r); err != nil { return nil, err } // Check for a valid ID. if r.Id == nil { return nil, errors.New("id is nil") } 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 } // unmarshalNotification attempts to unmarshal a marshaled JSON-RPC // notification (Request with a nil or no ID). func unmarshalNotification(s string) (btcjson.Cmd, error) { req, err := btcjson.ParseMarshaledCmd([]byte(s)) if err != nil { return nil, err } if req.Id() != nil { return nil, errors.New("id is non-nil") } return req, nil } // GetBestBlockResult holds the result of a getbestblock response. // // TODO(jrick): shove this in btcws. type GetBestBlockResult struct { Hash string `json:"hash"` Height int32 `json:"height"` } // GetBestBlock gets both the block height and hash of the best block // 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() } return response.Result().(*GetBestBlockResult), 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() } return response.Result().(*btcjson.BlockResult), 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() } return btcwire.BitcoinNet(uint32(response.Result().(float64))), 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() } // 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() } // 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() } // Rescan requests a blockchain rescan for transactions to any number of // addresses and notifications to inform wallet about such transactions. func Rescan(rpc ServerConn, beginBlock int32, addrs map[string]struct{}) *btcjson.Error { // NewRescanCmd cannot fail with no optargs, so omit the check. cmd, _ := btcws.NewRescanCmd(<-NewJSONID, beginBlock, addrs) request := NewServerRequest(cmd, nil) response := <-rpc.SendRequest(request) return response.Error() } // 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() } return *response.Result().(*string), nil }