diff --git a/api/client.go b/api/client.go index ebeb9c84..7e84bd29 100644 --- a/api/client.go +++ b/api/client.go @@ -1,6 +1,8 @@ package api import ( + "fmt" + "go.sia.tech/core/consensus" "go.sia.tech/core/types" "go.sia.tech/jape" @@ -64,3 +66,9 @@ func (c *Client) Tip() (resp types.ChainIndex, err error) { err = c.c.GET("/explorer/tip", &resp) return } + +// Block returns a block with the given block ID. +func (c *Client) Block(id types.BlockID) (resp types.Block, err error) { + err = c.c.GET(fmt.Sprintf("/explorer/block/%s", id), &resp) + return +} diff --git a/api/server.go b/api/server.go index 1fe97531..febaca75 100644 --- a/api/server.go +++ b/api/server.go @@ -40,6 +40,7 @@ type ( // Explorer implements a Sia explorer. Explorer interface { Tip() (types.ChainIndex, error) + Block(id types.BlockID) (types.Block, error) } ) @@ -132,10 +133,24 @@ func (s *server) txpoolBroadcastHandler(jc jape.Context) { func (s *server) explorerTipHandler(jc jape.Context) { tip, err := s.e.Tip() - jc.Check("failed to get tip", err) + if jc.Check("failed to get tip", err) != nil { + return + } jc.Encode(tip) } +func (s *server) explorerBlockHandler(jc jape.Context) { + var id types.BlockID + if jc.DecodeParam("id", &id) != nil { + return + } + block, err := s.e.Block(id) + if jc.Check("failed to get block", err) != nil { + return + } + jc.Encode(block) +} + // NewServer returns an HTTP handler that serves the explored API. func NewServer(e Explorer, cm ChainManager, s Syncer) http.Handler { srv := server{ @@ -152,6 +167,7 @@ func NewServer(e Explorer, cm ChainManager, s Syncer) http.Handler { "GET /txpool/fee": srv.txpoolFeeHandler, "POST /txpool/broadcast": srv.txpoolBroadcastHandler, - "GET /explorer/tip": srv.explorerTipHandler, + "GET /explorer/tip": srv.explorerTipHandler, + "GET /explorer/block/:id": srv.explorerBlockHandler, }) } diff --git a/explorer/explorer.go b/explorer/explorer.go index f0883788..78f3499b 100644 --- a/explorer/explorer.go +++ b/explorer/explorer.go @@ -11,6 +11,7 @@ type Store interface { chain.Subscriber Tip() (types.ChainIndex, error) + Block(id types.BlockID) (types.Block, error) } // Explorer implements a Sia explorer. @@ -26,3 +27,7 @@ func NewExplorer(s Store) *Explorer { func (e *Explorer) Tip() (types.ChainIndex, error) { return e.s.Tip() } + +func (e *Explorer) Block(id types.BlockID) (types.Block, error) { + return e.s.Block(id) +} diff --git a/persist/sqlite/query.go b/persist/sqlite/query.go index f49837f9..8e5648d2 100644 --- a/persist/sqlite/query.go +++ b/persist/sqlite/query.go @@ -1,6 +1,10 @@ package sqlite -import "go.sia.tech/core/types" +import ( + "time" + + "go.sia.tech/core/types" +) func decode(obj types.DecoderFrom, data []byte) error { d := types.NewBufDecoder(data) @@ -8,11 +12,64 @@ func decode(obj types.DecoderFrom, data []byte) error { return d.Err() } +func decodeUint64(x *uint64, data []byte) error { + d := types.NewBufDecoder(data) + if x != nil { + *x = d.ReadUint64() + } + return d.Err() +} + +// Tip implements explorer.Store. func (s *Store) Tip() (result types.ChainIndex, err error) { var data []byte if err = s.queryRow("SELECT id, height FROM Blocks WHERE height = (SELECT MAX(height) from Blocks)").Scan(&data, &result.Height); err != nil { return } - decode(&result.ID, data) + if err = decode(&result.ID, data); err != nil { + return + } + return +} + +// Block implements explorer.Store. +func (s *Store) Block(id types.BlockID) (result types.Block, err error) { + { + var timestamp int64 + var parentID, nonce []byte + if err = s.queryRow("SELECT parent_id, nonce, timestamp FROM Blocks WHERE id = ?", encode(id)).Scan(&parentID, &nonce, ×tamp); err != nil { + return + } + result.Timestamp = time.Unix(timestamp, 0).UTC() + if err = decode(&result.ParentID, parentID); err != nil { + return + } + if err = decodeUint64(&result.Nonce, nonce); err != nil { + return + } + } + + { + var rows *loggedRows + if rows, err = s.query("SELECT address, value FROM MinerPayouts WHERE block_id = ? ORDER BY block_order", encode(id)); err != nil { + return + } + + var address, value []byte + for rows.Next() { + if err = rows.Scan(&address, &value); err != nil { + return + } + var minerPayout types.SiacoinOutput + if err = decode(&minerPayout.Address, address); err != nil { + return + } + if err = decode(&minerPayout.Value, value); err != nil { + return + } + result.MinerPayouts = append(result.MinerPayouts, minerPayout) + } + } + return } diff --git a/persist/sqlite/txn.go b/persist/sqlite/txn.go index 1d7ae40e..fba3b3f7 100644 --- a/persist/sqlite/txn.go +++ b/persist/sqlite/txn.go @@ -24,6 +24,7 @@ func encodeUint64(x uint64) []byte { } func (s *Store) addBlock(b types.Block, height uint64) error { + // nonce is encoded because database/sql doesn't support uint64 with high bit set _, err := s.exec("INSERT INTO Blocks(id, height, parent_id, nonce, timestamp) VALUES (?, ?, ?, ?, ?);", encode(b.ID()), height, encode(b.ParentID), encodeUint64(b.Nonce), b.Timestamp.Unix()) return err }