Add websocket implementation WIP
parent
5af08584ab
commit
75e2ffa025
|
@ -11,6 +11,10 @@ type Metrics struct {
|
|||
SocketIOSubscribes *prometheus.CounterVec
|
||||
SocketIOClients prometheus.Gauge
|
||||
SocketIOReqDuration *prometheus.HistogramVec
|
||||
WebsocketRequests *prometheus.CounterVec
|
||||
WebsocketSubscribes *prometheus.CounterVec
|
||||
WebsocketClients prometheus.Gauge
|
||||
WebsocketReqDuration *prometheus.HistogramVec
|
||||
IndexResyncDuration prometheus.Histogram
|
||||
MempoolResyncDuration prometheus.Histogram
|
||||
TxCacheEfficiency *prometheus.CounterVec
|
||||
|
@ -48,7 +52,7 @@ func GetMetrics(coin string) (*Metrics, error) {
|
|||
metrics.SocketIOClients = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "blockbook_socketio_clients",
|
||||
Help: "Number of currently connected clients",
|
||||
Help: "Number of currently connected socketio clients",
|
||||
ConstLabels: Labels{"coin": coin},
|
||||
},
|
||||
)
|
||||
|
@ -61,6 +65,38 @@ func GetMetrics(coin string) (*Metrics, error) {
|
|||
},
|
||||
[]string{"method"},
|
||||
)
|
||||
metrics.WebsocketRequests = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "blockbook_websocket_requests",
|
||||
Help: "Total number of websocket requests by method and status",
|
||||
ConstLabels: Labels{"coin": coin},
|
||||
},
|
||||
[]string{"method", "status"},
|
||||
)
|
||||
metrics.WebsocketSubscribes = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "blockbook_websocket_subscribes",
|
||||
Help: "Total number of websocket subscribes by channel and status",
|
||||
ConstLabels: Labels{"coin": coin},
|
||||
},
|
||||
[]string{"channel", "status"},
|
||||
)
|
||||
metrics.WebsocketClients = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "blockbook_websocket_clients",
|
||||
Help: "Number of currently connected websocket clients",
|
||||
ConstLabels: Labels{"coin": coin},
|
||||
},
|
||||
)
|
||||
metrics.WebsocketReqDuration = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "blockbook_websocket_req_duration",
|
||||
Help: "Websocket request duration by method (in microseconds)",
|
||||
Buckets: []float64{1, 5, 10, 25, 50, 75, 100, 250},
|
||||
ConstLabels: Labels{"coin": coin},
|
||||
},
|
||||
[]string{"method"},
|
||||
)
|
||||
metrics.IndexResyncDuration = prometheus.NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "blockbook_index_resync_duration",
|
||||
|
|
|
@ -31,6 +31,7 @@ type PublicServer struct {
|
|||
binding string
|
||||
certFiles string
|
||||
socketio *SocketIoServer
|
||||
websocket *WebsocketServer
|
||||
https *http.Server
|
||||
db *db.RocksDB
|
||||
txCache *db.TxCache
|
||||
|
@ -59,6 +60,11 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
|
|||
return nil, err
|
||||
}
|
||||
|
||||
websocket, err := NewWebsocketServer(db, chain, txCache, metrics, is)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr, path := splitBinding(binding)
|
||||
serveMux := http.NewServeMux()
|
||||
https := &http.Server{
|
||||
|
@ -72,6 +78,7 @@ func NewPublicServer(binding string, certFiles string, db *db.RocksDB, chain bch
|
|||
https: https,
|
||||
api: api,
|
||||
socketio: socketio,
|
||||
websocket: websocket,
|
||||
db: db,
|
||||
txCache: txCache,
|
||||
chain: chain,
|
||||
|
@ -109,8 +116,9 @@ func (s *PublicServer) Run() error {
|
|||
func (s *PublicServer) ConnectFullPublicInterface() {
|
||||
serveMux := s.https.Handler.(*http.ServeMux)
|
||||
_, path := splitBinding(s.binding)
|
||||
// support for tests of socket.io interface
|
||||
serveMux.Handle(path+"test.html", http.FileServer(http.Dir("./static/")))
|
||||
// support for test pages
|
||||
serveMux.Handle(path+"test-socketio.html", http.FileServer(http.Dir("./static/")))
|
||||
serveMux.Handle(path+"test-websocket.html", http.FileServer(http.Dir("./static/")))
|
||||
if s.internalExplorer {
|
||||
// internal explorer handlers
|
||||
serveMux.HandleFunc(path+"tx/", s.htmlTemplateHandler(s.explorerTx))
|
||||
|
@ -136,6 +144,8 @@ func (s *PublicServer) ConnectFullPublicInterface() {
|
|||
serveMux.HandleFunc(path+"api/estimatefee/", s.jsonHandler(s.apiEstimateFee))
|
||||
// socket.io interface
|
||||
serveMux.Handle(path+"socket.io/", s.socketio.GetHandler())
|
||||
// websocket interface
|
||||
serveMux.Handle(path+"websocket", s.websocket.GetHandler())
|
||||
}
|
||||
|
||||
// Close closes the server
|
||||
|
@ -153,6 +163,7 @@ func (s *PublicServer) Shutdown(ctx context.Context) error {
|
|||
// OnNewBlock notifies users subscribed to bitcoind/hashblock about new block
|
||||
func (s *PublicServer) OnNewBlock(hash string, height uint32) {
|
||||
s.socketio.OnNewBlockHash(hash)
|
||||
s.websocket.OnNewBlock(hash, height)
|
||||
}
|
||||
|
||||
// OnNewTxAddr notifies users subscribed to bitcoind/addresstxid about new block
|
||||
|
|
|
@ -103,13 +103,6 @@ var onMessageHandlers = map[string]func(*SocketIoServer, json.RawMessage) (inter
|
|||
}
|
||||
return
|
||||
},
|
||||
"getAccountInfo": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) {
|
||||
req, err := unmarshalGetAccountInfoRequest(params)
|
||||
if err == nil {
|
||||
rv, err = s.getAccountInfo(req)
|
||||
}
|
||||
return
|
||||
},
|
||||
"getBlockHeader": func(s *SocketIoServer, params json.RawMessage) (rv interface{}, err error) {
|
||||
height, hash, err := unmarshalGetBlockHeader(params)
|
||||
if err == nil {
|
||||
|
@ -214,22 +207,6 @@ func unmarshalGetAddressRequest(params []byte) (addr []string, opts addrOpts, er
|
|||
return
|
||||
}
|
||||
|
||||
type accountInfoReq struct {
|
||||
Descriptor string `json:"descriptor"`
|
||||
Details string `json:"details"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Page int `json:"page"`
|
||||
}
|
||||
|
||||
func unmarshalGetAccountInfoRequest(params []byte) (*accountInfoReq, error) {
|
||||
var r accountInfoReq
|
||||
err := json.Unmarshal(params, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
type resultAddressTxids struct {
|
||||
Result []string `json:"result"`
|
||||
}
|
||||
|
@ -684,24 +661,6 @@ func (s *SocketIoServer) getMempoolEntry(txid string) (res resultGetMempoolEntry
|
|||
return
|
||||
}
|
||||
|
||||
func (s *SocketIoServer) getAccountInfo(req *accountInfoReq) (res *api.Address, err error) {
|
||||
if s.chainParser.GetChainType() == bchain.ChainEthereumType {
|
||||
var opt api.GetAddressOption
|
||||
switch req.Details {
|
||||
case "balance":
|
||||
opt = api.Balance
|
||||
case "txids":
|
||||
opt = api.TxidHistory
|
||||
case "txs":
|
||||
opt = api.TxHistory
|
||||
default:
|
||||
opt = api.Basic
|
||||
}
|
||||
return s.api.GetAddress(req.Descriptor, req.Page, req.PageSize, opt, api.AddressFilterNone)
|
||||
}
|
||||
return nil, errors.New("Not implemented")
|
||||
}
|
||||
|
||||
// onSubscribe expects two event subscriptions based on the req parameter (including the doublequotes):
|
||||
// "bitcoind/hashblock"
|
||||
// "bitcoind/addresstxid",["2MzTmvPJLZaLzD9XdN3jMtQA5NexC3rAPww","2NAZRJKr63tSdcTxTN3WaE9ZNDyXy6PgGuv"]
|
||||
|
|
|
@ -0,0 +1,348 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"blockbook/api"
|
||||
"blockbook/bchain"
|
||||
"blockbook/common"
|
||||
"blockbook/db"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/juju/errors"
|
||||
)
|
||||
|
||||
const upgradeFailed = "Upgrade failed: "
|
||||
const outChannelSize = 500
|
||||
const defaultTimeout = 60 * time.Second
|
||||
|
||||
var (
|
||||
// ErrorMethodNotAllowed is returned when client tries to upgrade method other than GET
|
||||
ErrorMethodNotAllowed = errors.New("Method not allowed")
|
||||
|
||||
connectionCounter uint64
|
||||
)
|
||||
|
||||
type websocketReq struct {
|
||||
ID string `json:"id"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params"`
|
||||
}
|
||||
|
||||
type websocketRes struct {
|
||||
ID string `json:"id"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type websocketChannel struct {
|
||||
id uint64
|
||||
conn *websocket.Conn
|
||||
out chan *websocketRes
|
||||
ip string
|
||||
requestHeader http.Header
|
||||
alive bool
|
||||
aliveLock sync.Mutex
|
||||
}
|
||||
|
||||
// WebsocketServer is a handle to websocket server
|
||||
type WebsocketServer struct {
|
||||
socket *websocket.Conn
|
||||
upgrader *websocket.Upgrader
|
||||
db *db.RocksDB
|
||||
txCache *db.TxCache
|
||||
chain bchain.BlockChain
|
||||
chainParser bchain.BlockChainParser
|
||||
metrics *common.Metrics
|
||||
is *common.InternalState
|
||||
api *api.Worker
|
||||
newBlockSubscriptions map[*websocketChannel]string
|
||||
newBlockSubscriptionsLock sync.Mutex
|
||||
addressSubscriptions map[string]map[*websocketChannel]string
|
||||
addressSubscriptionsLock sync.Mutex
|
||||
}
|
||||
|
||||
// NewWebsocketServer creates new websocket interface to blockbook and returns its handle
|
||||
func NewWebsocketServer(db *db.RocksDB, chain bchain.BlockChain, txCache *db.TxCache, metrics *common.Metrics, is *common.InternalState) (*WebsocketServer, error) {
|
||||
api, err := api.NewWorker(db, chain, txCache, is)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &WebsocketServer{
|
||||
upgrader: &websocket.Upgrader{
|
||||
ReadBufferSize: 1024 * 32,
|
||||
WriteBufferSize: 1024 * 32,
|
||||
},
|
||||
db: db,
|
||||
txCache: txCache,
|
||||
chain: chain,
|
||||
chainParser: chain.GetChainParser(),
|
||||
metrics: metrics,
|
||||
is: is,
|
||||
api: api,
|
||||
newBlockSubscriptions: make(map[*websocketChannel]string),
|
||||
addressSubscriptions: make(map[string]map[*websocketChannel]string),
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ServeHTTP sets up handler of websocket channel
|
||||
func (s *WebsocketServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, upgradeFailed+ErrorMethodNotAllowed.Error(), 503)
|
||||
return
|
||||
}
|
||||
conn, err := s.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
http.Error(w, upgradeFailed+err.Error(), 503)
|
||||
return
|
||||
}
|
||||
c := &websocketChannel{
|
||||
id: atomic.AddUint64(&connectionCounter, 1),
|
||||
conn: conn,
|
||||
out: make(chan *websocketRes, outChannelSize),
|
||||
ip: r.RemoteAddr,
|
||||
requestHeader: r.Header,
|
||||
alive: true,
|
||||
}
|
||||
go s.inputLoop(c)
|
||||
go s.outputLoop(c)
|
||||
s.onConnect(c)
|
||||
}
|
||||
|
||||
// GetHandler returns http handler
|
||||
func (s *WebsocketServer) GetHandler() http.Handler {
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *WebsocketServer) closeChannel(c *websocketChannel) {
|
||||
c.aliveLock.Lock()
|
||||
defer c.aliveLock.Unlock()
|
||||
if c.alive {
|
||||
c.conn.Close()
|
||||
c.alive = false
|
||||
//clean out
|
||||
close(c.out)
|
||||
for len(c.out) > 0 {
|
||||
<-c.out
|
||||
}
|
||||
s.onDisconnect(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *websocketChannel) IsAlive() bool {
|
||||
c.aliveLock.Lock()
|
||||
defer c.aliveLock.Unlock()
|
||||
return c.alive
|
||||
}
|
||||
|
||||
func (s *WebsocketServer) inputLoop(c *websocketChannel) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
glog.Error("recovered from panic: ", r, ", ", c.id)
|
||||
debug.PrintStack()
|
||||
s.closeChannel(c)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
t, d, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
s.closeChannel(c)
|
||||
return
|
||||
}
|
||||
switch t {
|
||||
case websocket.TextMessage:
|
||||
var req websocketReq
|
||||
err := json.Unmarshal(d, &req)
|
||||
if err != nil {
|
||||
glog.Error("Error parsing message from ", c.id, ", ", string(d), ", ", err)
|
||||
s.closeChannel(c)
|
||||
return
|
||||
}
|
||||
go s.onRequest(c, &req)
|
||||
case websocket.BinaryMessage:
|
||||
glog.Error("Binary message received from ", c.id, ", ", c.ip)
|
||||
s.closeChannel(c)
|
||||
return
|
||||
case websocket.PingMessage:
|
||||
c.conn.WriteControl(websocket.PongMessage, nil, time.Now().Add(defaultTimeout))
|
||||
break
|
||||
case websocket.CloseMessage:
|
||||
s.closeChannel(c)
|
||||
return
|
||||
case websocket.PongMessage:
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebsocketServer) outputLoop(c *websocketChannel) {
|
||||
for m := range c.out {
|
||||
err := c.conn.WriteJSON(m)
|
||||
if err != nil {
|
||||
glog.Error("Error sending message to ", c.id, ", ", err)
|
||||
s.closeChannel(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebsocketServer) onConnect(c *websocketChannel) {
|
||||
glog.Info("Client connected ", c.id, ", ", c.ip)
|
||||
s.metrics.WebsocketClients.Inc()
|
||||
}
|
||||
|
||||
func (s *WebsocketServer) onDisconnect(c *websocketChannel) {
|
||||
s.unsubscribeNewBlock(c)
|
||||
glog.Info("Client disconnected ", c.id, ", ", c.ip)
|
||||
s.metrics.WebsocketClients.Dec()
|
||||
}
|
||||
|
||||
var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *websocketReq) (interface{}, error){
|
||||
"getAccountInfo": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) {
|
||||
r, err := unmarshalGetAccountInfoRequest(req.Params)
|
||||
if err == nil {
|
||||
rv, err = s.getAccountInfo(r)
|
||||
}
|
||||
return
|
||||
},
|
||||
"sendTransaction": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) {
|
||||
r := struct {
|
||||
Hex string `json:"hex"`
|
||||
}{}
|
||||
err = json.Unmarshal(req.Params, &r)
|
||||
if err == nil {
|
||||
rv, err = s.sendTransaction(r.Hex)
|
||||
}
|
||||
return
|
||||
},
|
||||
"subscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) {
|
||||
rv, err = s.subscribeNewBlock(c, req)
|
||||
return
|
||||
},
|
||||
"unsubscribeNewBlock": func(s *WebsocketServer, c *websocketChannel, req *websocketReq) (rv interface{}, err error) {
|
||||
rv, err = s.unsubscribeNewBlock(c)
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
func (s *WebsocketServer) onRequest(c *websocketChannel, req *websocketReq) {
|
||||
var err error
|
||||
var data interface{}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
glog.Error("Client ", c.id, ", onRequest ", req.Method, " recovered from panic: ", r)
|
||||
debug.PrintStack()
|
||||
e := resultError{}
|
||||
e.Error.Message = "Internal error"
|
||||
data = e
|
||||
}
|
||||
// nil data means no response
|
||||
if data != nil {
|
||||
c.out <- &websocketRes{
|
||||
ID: req.ID,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
}()
|
||||
t := time.Now()
|
||||
defer s.metrics.WebsocketReqDuration.With(common.Labels{"method": req.Method}).Observe(float64(time.Since(t)) / 1e3) // in microseconds
|
||||
f, ok := requestHandlers[req.Method]
|
||||
if ok {
|
||||
data, err = f(s, c, req)
|
||||
} else {
|
||||
err = errors.New("unknown method")
|
||||
}
|
||||
if err == nil {
|
||||
glog.V(1).Info("Client ", c.id, " onRequest ", req.Method, " success")
|
||||
s.metrics.SocketIORequests.With(common.Labels{"method": req.Method, "status": "success"}).Inc()
|
||||
} else {
|
||||
glog.Error("Client ", c.id, " onMessage ", req.Method, ": ", errors.ErrorStack(err))
|
||||
s.metrics.SocketIORequests.With(common.Labels{"method": req.Method, "status": err.Error()}).Inc()
|
||||
e := resultError{}
|
||||
e.Error.Message = err.Error()
|
||||
data = e
|
||||
}
|
||||
}
|
||||
|
||||
type accountInfoReq struct {
|
||||
Descriptor string `json:"descriptor"`
|
||||
Details string `json:"details"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Page int `json:"page"`
|
||||
}
|
||||
|
||||
func unmarshalGetAccountInfoRequest(params []byte) (*accountInfoReq, error) {
|
||||
var r accountInfoReq
|
||||
err := json.Unmarshal(params, &r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
func (s *WebsocketServer) getAccountInfo(req *accountInfoReq) (res *api.Address, err error) {
|
||||
if s.chainParser.GetChainType() == bchain.ChainEthereumType {
|
||||
var opt api.GetAddressOption
|
||||
switch req.Details {
|
||||
case "balance":
|
||||
opt = api.Balance
|
||||
case "txids":
|
||||
opt = api.TxidHistory
|
||||
case "txs":
|
||||
opt = api.TxHistory
|
||||
default:
|
||||
opt = api.Basic
|
||||
}
|
||||
return s.api.GetAddress(req.Descriptor, req.Page, req.PageSize, opt, api.AddressFilterNone)
|
||||
}
|
||||
return nil, errors.New("Not implemented")
|
||||
}
|
||||
|
||||
func (s *WebsocketServer) sendTransaction(tx string) (res resultSendTransaction, err error) {
|
||||
txid, err := s.chain.SendRawTransaction(tx)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res.Result = txid
|
||||
return
|
||||
}
|
||||
|
||||
func (s *WebsocketServer) subscribeNewBlock(c *websocketChannel, req *websocketReq) (res interface{}, err error) {
|
||||
s.newBlockSubscriptionsLock.Lock()
|
||||
defer s.newBlockSubscriptionsLock.Unlock()
|
||||
s.newBlockSubscriptions[c] = req.ID
|
||||
return
|
||||
}
|
||||
|
||||
func (s *WebsocketServer) unsubscribeNewBlock(c *websocketChannel) (res interface{}, err error) {
|
||||
s.newBlockSubscriptionsLock.Lock()
|
||||
defer s.newBlockSubscriptionsLock.Unlock()
|
||||
delete(s.newBlockSubscriptions, c)
|
||||
return
|
||||
}
|
||||
|
||||
// OnNewBlock is a callback that broadcasts info about new block to subscribed clients
|
||||
func (s *WebsocketServer) OnNewBlock(hash string, height uint32) {
|
||||
s.newBlockSubscriptionsLock.Lock()
|
||||
defer s.newBlockSubscriptionsLock.Unlock()
|
||||
data := struct {
|
||||
Height uint32 `json:"height"`
|
||||
Hash string `json:"hash"`
|
||||
}{
|
||||
Height: height,
|
||||
Hash: hash,
|
||||
}
|
||||
for c, id := range s.newBlockSubscriptions {
|
||||
if c.IsAlive() {
|
||||
c.out <- &websocketRes{
|
||||
ID: id,
|
||||
Data: &data,
|
||||
}
|
||||
}
|
||||
}
|
||||
glog.Info("broadcasting new block ", height, " ", hash, " to ", len(s.newBlockSubscriptions), " channels")
|
||||
}
|
|
@ -11,7 +11,7 @@
|
|||
}
|
||||
</style>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.7.4/socket.io.js"></script>
|
||||
<title>Test socket.io</title>
|
||||
<title>Blockbook test socket.io</title>
|
||||
<script>
|
||||
var socket;
|
||||
function connect(server) {
|
||||
|
@ -51,26 +51,6 @@
|
|||
return socket.send({ method, params }, f);
|
||||
}
|
||||
|
||||
function getAccountInfo() {
|
||||
const descriptor = document.getElementById('getAccountInfoDescriptor').value.trim();
|
||||
const selectDetails = document.getElementById('getAccountInfoDetails');
|
||||
const details = selectDetails.options[selectDetails.selectedIndex].value;
|
||||
const page = parseInt(document.getElementById("getAccountInfoPage").value);
|
||||
const pageSize = 10;
|
||||
const method = 'getAccountInfo';
|
||||
const params = {
|
||||
descriptor,
|
||||
details,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
socket.send({ method, params}, function (result) {
|
||||
console.log('getAccountInfo sent successfully');
|
||||
console.log(result);
|
||||
document.getElementById('getAccountInfoResult').innerText = JSON.stringify(result).replace(/,/g, ", ");
|
||||
});
|
||||
}
|
||||
|
||||
function getAddressHistory() {
|
||||
var addresses = document.getElementById('getAddressHistoryAddresses').value.split(",");
|
||||
addresses = addresses.map(s => s.trim());
|
||||
|
@ -284,28 +264,6 @@
|
|||
<label id="connectionStatus">not connected</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="getAccountInfo" onclick="getAccountInfo()">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div class="row" style="margin: 0;">
|
||||
<input type="text" style="width: 67.7%" class="form-control" id="getAccountInfoDescriptor" value="0x103262f243e6f67d12d6a4ea0d45302c1fa4bb0a">
|
||||
<select id="getAccountInfoDetails" style="width: 20%; margin-left: 5px;">
|
||||
<option value="basic">Basic</option>
|
||||
<option value="balance">Balance</option>
|
||||
<option value="txids">Txids</option>
|
||||
<option value="txs">Transactions</option>
|
||||
</select>
|
||||
<input type="text" style="width: 10%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getAccountInfoPage" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col form-inline"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col" id="getAccountInfoResult">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="getAddressTxids" onclick="getAddressTxids()">
|
|
@ -0,0 +1,305 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<style>
|
||||
.row {
|
||||
margin-top: 1%;
|
||||
}
|
||||
</style>
|
||||
<title>Blockbook test websocket</title>
|
||||
<script>
|
||||
var ws;
|
||||
var messageID;
|
||||
var pendingMessages;
|
||||
var subscriptions;
|
||||
function send(method, params, callback) {
|
||||
id = messageID.toString();
|
||||
messageID++;
|
||||
pendingMessages[id] = callback;
|
||||
var req = {
|
||||
id,
|
||||
method,
|
||||
params
|
||||
}
|
||||
ws.send(JSON.stringify(req));
|
||||
return id;
|
||||
}
|
||||
function subscribe(method, params, callback) {
|
||||
id = messageID.toString();
|
||||
messageID++;
|
||||
subscriptions[id] = callback;
|
||||
var req = {
|
||||
id,
|
||||
method,
|
||||
params
|
||||
}
|
||||
ws.send(JSON.stringify(req));
|
||||
return id;
|
||||
}
|
||||
function connect(server) {
|
||||
messageID = 0;
|
||||
pendingMessages = {}
|
||||
subscriptions = {}
|
||||
if (server.startsWith("http")) {
|
||||
server.replace("http", "ws");
|
||||
}
|
||||
if (!server.endsWith("/websocket")) {
|
||||
server += "/websocket";
|
||||
}
|
||||
ws = new WebSocket(server);
|
||||
ws.onopen = function (e) {
|
||||
console.log('socket connected', e);
|
||||
document.getElementById('connectionStatus').innerText = "connected";
|
||||
};
|
||||
ws.onclose = function (e) {
|
||||
console.log('socket closed', e);
|
||||
document.getElementById('connectionStatus').innerText = "disconnected";
|
||||
};
|
||||
ws.onerror = function (e) {
|
||||
console.log('socket error ', e);
|
||||
document.getElementById('connectionStatus').innerText = "error";
|
||||
};
|
||||
ws.onmessage = function (e) {
|
||||
console.log('resp ' + e.data);
|
||||
var resp = JSON.parse(e.data);
|
||||
var f = pendingMessages[resp.id];
|
||||
if (f != undefined) {
|
||||
delete pendingMessages[resp.id];
|
||||
f(resp.data);
|
||||
} else {
|
||||
f = subscriptions[resp.id];
|
||||
if (f != undefined) {
|
||||
f(resp.data);
|
||||
}
|
||||
else {
|
||||
console.log("unkown response " + resp.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getAccountInfo() {
|
||||
const descriptor = document.getElementById('getAccountInfoDescriptor').value.trim();
|
||||
const selectDetails = document.getElementById('getAccountInfoDetails');
|
||||
const details = selectDetails.options[selectDetails.selectedIndex].value;
|
||||
const page = parseInt(document.getElementById("getAccountInfoPage").value);
|
||||
const pageSize = 10;
|
||||
const method = 'getAccountInfo';
|
||||
const params = {
|
||||
descriptor,
|
||||
details,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
send(method, params, function (result) {
|
||||
document.getElementById('getAccountInfoResult').innerText = JSON.stringify(result).replace(/,/g, ", ");
|
||||
});
|
||||
}
|
||||
|
||||
function sendTransaction() {
|
||||
var hex = document.getElementById('sendTransactionHex').value.trim();
|
||||
const method = 'sendTransaction';
|
||||
const params = {
|
||||
hex,
|
||||
};
|
||||
send(method, params, function (result) {
|
||||
document.getElementById('sendTransactionResult').innerText = JSON.stringify(result).replace(/,/g, ", ");
|
||||
});
|
||||
}
|
||||
|
||||
function subscribeNewBlock() {
|
||||
const method = 'subscribeNewBlock';
|
||||
const params = {
|
||||
};
|
||||
var id = subscribe(method, params, function (result) {
|
||||
document.getElementById('subscribeNewBlockResult').innerText += JSON.stringify(result).replace(/,/g, ", ") + "\n";
|
||||
});
|
||||
document.getElementById('subscribeNewBlockId').innerText = id;
|
||||
}
|
||||
|
||||
function subscribeAddress() {
|
||||
const method = 'subscribeAddress';
|
||||
var address = document.getElementById('subscribeAddressName').value;
|
||||
const params = {
|
||||
address
|
||||
};
|
||||
var id = subscribe(method, params, function (result) {
|
||||
document.getElementById('subscribeAddressResult').innerText += JSON.stringify(result).replace(/,/g, ", ") + "\n";
|
||||
});
|
||||
document.getElementById('subscribeAddressIds').innerText += id+" ";
|
||||
}
|
||||
|
||||
function getBlockHeader() {
|
||||
var param = document.getElementById('getBlockHeaderParam').value.trim();
|
||||
lookupBlockHash(isHash(param) ? param : parseInt(param), function (result) {
|
||||
console.log('getBlockHeader sent successfully');
|
||||
console.log(result);
|
||||
document.getElementById('getBlockHeaderResult').innerText = JSON.stringify(result).replace(/,/g, ", ");
|
||||
});
|
||||
}
|
||||
|
||||
function isHash(str) {
|
||||
var re = /[0-9A-Fa-f]{64}/g;
|
||||
return re.test(str);
|
||||
}
|
||||
|
||||
function lookupBlockHash(heightOrHash, f) {
|
||||
const method = 'getBlockHeader';
|
||||
const params = [heightOrHash];
|
||||
return socket.send({ method, params }, f);
|
||||
}
|
||||
|
||||
function estimateFee() {
|
||||
var blocks = document.getElementById('estimateFeeBlocks').value.trim();
|
||||
estimateTxFee(parseInt(blocks), function (result) {
|
||||
console.log('estimateFee sent successfully');
|
||||
console.log(result);
|
||||
document.getElementById('estimateFeeResult').innerText = JSON.stringify(result).replace(/,/g, ", ");
|
||||
});
|
||||
}
|
||||
|
||||
function estimateTxFee(blocks, f) {
|
||||
const method = 'estimateFee';
|
||||
const params = [blocks];
|
||||
return socket.send({ method, params }, f);
|
||||
}
|
||||
|
||||
function getInfo() {
|
||||
lookupSyncStatus(function (result) {
|
||||
console.log('getInfo sent successfully');
|
||||
console.log(result);
|
||||
document.getElementById('getInfoResult').innerText = JSON.stringify(result).replace(/,/g, ", ");
|
||||
});
|
||||
}
|
||||
|
||||
function lookupSyncStatus(f) {
|
||||
const method = 'getInfo';
|
||||
const params = [];
|
||||
return socket.send({ method, params }, f);
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<h1>Socket.io tester</h1>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="Login" onclick="connect(document.getElementById('serverAddress').value)">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input type="text" class="form-control" id="serverAddress" value="">
|
||||
</div>
|
||||
<div class="col form-inline">
|
||||
<label id="connectionStatus">not connected</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="getAccountInfo" onclick="getAccountInfo()">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<div class="row" style="margin: 0;">
|
||||
<input type="text" style="width: 67.7%" class="form-control" id="getAccountInfoDescriptor" value="0x103262f243e6f67d12d6a4ea0d45302c1fa4bb0a">
|
||||
<select id="getAccountInfoDetails" style="width: 20%; margin-left: 5px;">
|
||||
<option value="basic">Basic</option>
|
||||
<option value="balance">Balance</option>
|
||||
<option value="txids">Txids</option>
|
||||
<option value="txs">Transactions</option>
|
||||
</select>
|
||||
<input type="text" style="width: 10%; margin-left: 5px; margin-right: 5px;" class="form-control" id="getAccountInfoPage" value="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col form-inline"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col" id="getAccountInfoResult">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="getBlockHeader" onclick="getBlockHeader()">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input type="text" class="form-control" id="getBlockHeaderParam" value="0">
|
||||
</div>
|
||||
<div class="col">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col" id="getBlockHeaderResult">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="estimateFee" onclick="estimateFee()">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input type="text" class="form-control" id="estimateFeeBlocks" value="20">
|
||||
</div>
|
||||
<div class="col"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col" id="estimateFeeResult">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="getInfo" onclick="getInfo()">
|
||||
</div>
|
||||
<div class="col-10" id="getInfoResult">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="sendTransaction" onclick="sendTransaction()">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input type="text" class="form-control" id="sendTransactionHex" value="010000000001019d64f0c72a0d206001decbffaa722eb1044534c74eee7a5df8318e42a4323ec10000000017160014550da1f5d25a9dae2eafd6902b4194c4c6500af6ffffffff02809698000000000017a914cd668d781ece600efa4b2404dc91fd26b8b8aed8870553d7360000000017a914246655bdbd54c7e477d0ea2375e86e0db2b8f80a8702473044022076aba4ad559616905fa51d4ddd357fc1fdb428d40cb388e042cdd1da4a1b7357022011916f90c712ead9a66d5f058252efd280439ad8956a967e95d437d246710bc9012102a80a5964c5612bb769ef73147b2cf3c149bc0fd4ecb02f8097629c94ab013ffd00000000">
|
||||
</div>
|
||||
<div class="col">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col" id="sendTransactionResult"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="subscribe new block" onclick="subscribeNewBlock()">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<span id="subscribeNewBlockId"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col" id="subscribeNewBlockResult"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="btn btn-secondary" type="button" value="subscribe address" onclick="subscribeAddress()">
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input type="text" class="form-control" id="subscribeAddressName" value="2MzTmvPJLZaLzD9XdN3jMtQA5NexC3rAPww">
|
||||
</div>
|
||||
<div class="col">
|
||||
<span id="subscribeAddressIds"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col" id="subscribeAddressTxidResult">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
document.getElementById('serverAddress').value = window.location.protocol.replace("http", "ws") + "//" + window.location.host;
|
||||
</script>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue