chainwatch: very, very basic key info view

This commit is contained in:
Łukasz Magiera 2019-11-16 20:47:06 +01:00
parent 3dd9691467
commit d235e7f20e
7 changed files with 300 additions and 18 deletions

View File

@ -66,10 +66,6 @@ var runCmd = &cli.Command{
log.Info("Remote version: %s", v.Version)
//http.Handle("/", http.FileServer(rice.MustFindBox("site").HTTPBox()))
fmt.Printf("Open http://%s\n", cctx.String("front"))
st, err := openStorage()
if err != nil {
return err
@ -78,6 +74,15 @@ var runCmd = &cli.Command{
runSyncer(ctx, api, st)
h, err := newHandler(api, st)
if err != nil {
return err
}
http.Handle("/", h)
fmt.Printf("Open http://%s\n", cctx.String("front"))
go func() {
<-ctx.Done()
os.Exit(0)

View File

@ -13,13 +13,18 @@
</div>
<div class="Index-nodes">
<div class="Index-node">
X Actors; Y Miners; Z Power (A Total; B Slashed);
<b>{{countCol "actors" "id"}}</b> Actors;
<b>{{countCol "miner_heads" "addr"}}</b> Miners;
<b>{{netPower "slashed_at = 0" | sizeStr}}</b> Power
(<b>{{netPower "" | sizeStr}}</b> Total;
<b>{{netPower "slashed_at > 0" | sizeStr}}</b> Slashed)
</div>
<div class="Index-node">
U Messages; V Gas Used; Total D FIL transferred; E state changes
{{count "messages"}} Messages; {{count "actors"}} state changes
</div>
<div class="Index-node">
N Wallets; E% FIL in wallets; F% FIL in miners; M% in market; %G Other actors; %H FIL it treasury
{{count "id_address_map" "id != address"}} <a href="keys.html">Keys</a>;
E% FIL in wallets; F% FIL in miners; M% in market; %G Other actors; %H FIL it treasury
</div>
</div>
</div>

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>Lotus ChainWatch</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
{{$wallet := param "w"}}
<div class="Index">
<div class="Index-header">
<div>
<span>Lotus ChainWatch - Wallet {{$wallet}}</span>
</div>
</div>
<div class="Index-nodes">
<div class="Index-node">
Balance: {{queryNum "select balance from actors inner join main.id_address_map m on m.address = ? where actors.id = m.id order by nonce desc limit 1" $wallet }}
</div>
<div class="Index-node">
Messages:
{{ range strings "messages" "case when `to` = ? then `from` else `to` end" "`from` = ? or `to` = ?" $wallet $wallet $wallet}}
<div>to/from {{.}}</div>
{{end}}
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Lotus ChainWatch</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
<div class="Index">
<div class="Index-header">
<div>
<span>Lotus ChainWatch - Wallets</span>
</div>
</div>
<div class="Index-nodes">
<div class="Index-node">
{{range strings "id_address_map" "address" "address != id"}}
<div><a href="key.html?w={{.}}">{{.}}</a></div>
{{end}}
</div>
</div>
</div>
</body>
</html>

View File

@ -12,6 +12,7 @@ body {
background: #1a1a1a;
color: #f0f0f0;
font-family: monospace;
overflow: auto;
display: grid;
grid-template-columns: auto 80vw auto;

View File

@ -31,21 +31,24 @@ func (st *storage) setup() error {
return err
}
_, err = tx.Exec(`
create table actors
create table if not exists actors
(
id text not null,
code text not null,
head text not null,
nonce int not null,
balance text,
balance text not null,
stateroot text
constraint actors_blocks_stateroot_fk
references blocks (parentStateRoot),
constraint actors_pk
primary key (id, nonce, balance, stateroot)
);
create index if not exists actors_id_index
on actors (id);
create table id_address_map
create table if not exists id_address_map
(
id text not null
constraint id_address_map_actors_id_fk
@ -55,7 +58,13 @@ create table id_address_map
primary key (id, address)
);
create table messages
create index if not exists id_address_map_address_index
on id_address_map (address);
create index if not exists id_address_map_id_index
on id_address_map (id);
create table if not exists messages
(
cid text not null
constraint messages_pk
@ -74,10 +83,10 @@ create table messages
params blob
);
create unique index messages_cid_uindex
create unique index if not exists messages_cid_uindex
on messages (cid);
create table blocks
create table if not exists blocks
(
cid text not null
constraint blocks_pk
@ -91,10 +100,10 @@ create table blocks
timestamp int not null
);
create unique index blocks_cid_uindex
create unique index if not exists blocks_cid_uindex
on blocks (cid);
create table block_messages
create table if not exists block_messages
(
block text not null
constraint block_messages_blk_fk
@ -106,7 +115,7 @@ create table block_messages
primary key (block, message)
);
create table miner_heads
create table if not exists miner_heads
(
head text not null
constraint miner_heads_actors_head_fk
@ -127,8 +136,10 @@ create table miner_heads
active int,
ppe int not null,
slashed_at int not null,
constraint miner_heads_id_address_map_address_address_fk
foreign key (owner, worker) references id_address_map (address, address),
constraint miner_heads_id_address_map_owner_fk
foreign key (owner) references id_address_map (address),
constraint miner_heads_id_address_map_worker_fk
foreign key (worker) references id_address_map (address),
constraint miner_heads_pk
primary key (head, addr)
);

View File

@ -0,0 +1,208 @@
package main
import (
"fmt"
rice "github.com/GeertJohan/go.rice"
"github.com/filecoin-project/lotus/chain/types"
"golang.org/x/xerrors"
"html/template"
"net/http"
"os"
"path/filepath"
"github.com/filecoin-project/lotus/api"
)
type handler struct {
api api.FullNode
st *storage
site *rice.Box
assets http.Handler
templates map[string]*template.Template
}
func newHandler(api api.FullNode, st *storage) (*handler, error) {
h := &handler{
api: api,
st: st,
site: rice.MustFindBox("site"),
templates: map[string]*template.Template{},
}
h.assets = http.FileServer(h.site.HTTPBox())
funcs := template.FuncMap{
"count": h.count,
"countCol": h.countCol,
"sum": h.sum,
"netPower": h.netPower,
"queryNum": h.queryNum,
"sizeStr": sizeStr,
"strings": h.strings,
"param": func(string) string { return "" }, // replaced in request handler
}
base := template.New("")
base.Funcs(funcs)
return h, h.site.Walk(".", func(path string, info os.FileInfo, err error) error {
if filepath.Ext(path) != ".html" {
return nil
}
if err != nil {
return err
}
log.Info(path)
h.templates["/"+path], err = base.New(path).Parse(h.site.MustString(path))
return err
})
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h, err := newHandler(h.api, h.st) // for faster dev
if err != nil {
log.Error(err)
return
}
p := r.URL.Path
if p == "/" {
p = "/index.html"
}
t, ok := h.templates[p]
if !ok {
h.assets.ServeHTTP(w, r)
return
}
t, err = t.Clone()
if err != nil {
log.Error(err)
return
}
t.Funcs(map[string]interface{}{
"param": r.FormValue,
})
if err := t.Execute(w, nil); err != nil {
log.Error(err)
return
}
log.Info(r.URL.Path)
}
// Template funcs
func (h *handler) count(table string, filters ...string) (int, error) {
// explicitly not caring about sql injection too much, this doesn't take user input
filts := ""
if len(filters) > 0 {
filts = " where "
for _, filter := range filters {
filts += filter + " and "
}
filts = filts[:len(filts)-5]
}
var c int
err := h.st.db.QueryRow("select count(1) from " + table + filts).Scan(&c)
if err != nil {
return 0, err
}
return c, nil
}
func (h *handler) countCol(table string, col string, filters ...string) (int, error) {
// explicitly not caring about sql injection too much, this doesn't take user input
filts := ""
if len(filters) > 0 {
filts = " where "
for _, filter := range filters {
filts += filter + " and "
}
filts = filts[:len(filts)-5]
}
var c int
err := h.st.db.QueryRow("select count(distinct " + col + ") from " + table + filts).Scan(&c)
if err != nil {
return 0, err
}
return c, nil
}
func (h *handler) sum(table string, col string) (types.BigInt, error) {
return h.queryNum("select sum(cast(" + col + " as bigint)) from " + table)
}
func (h *handler) netPower(slashFilt string) (types.BigInt, error) {
if slashFilt != "" {
slashFilt = " where " + slashFilt
}
return h.queryNum(`select sum(power) from (
select miner_heads.power, miner_heads.slashed_at, max(height) from miner_heads
inner join blocks b on miner_heads.stateroot = b.parentStateRoot
group by miner_heads.addr)` + slashFilt)
}
func (h *handler) queryNum(q string, p ...interface{}) (types.BigInt, error) {
// explicitly not caring about sql injection too much, this doesn't take user input
var c string
err := h.st.db.QueryRow(q, p...).Scan(&c)
if err != nil {
log.Error("qnum ", q, p, err)
return types.NewInt(0), err
}
i := types.NewInt(0)
_, ok := i.SetString(c, 10)
if !ok {
return types.NewInt(0), xerrors.New("num parse error: " + c)
}
return i, nil
}
var units = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"}
func sizeStr(size types.BigInt) string {
size = types.BigMul(size, types.NewInt(100))
i := 0
for types.BigCmp(size, types.NewInt(102400)) >= 0 && i < len(units)-1 {
size = types.BigDiv(size, types.NewInt(1024))
i++
}
return fmt.Sprintf("%s.%s %s", types.BigDiv(size, types.NewInt(100)), types.BigMod(size, types.NewInt(100)), units[i])
}
func (h *handler) strings(table string, col string, filter string, args ...interface{}) (out []string, err error) {
if len(filter) > 0 {
filter = " where " + filter
}
log.Info("strings qstr ", "select "+col+" from "+table+filter)
rws, err := h.st.db.Query("select "+col+" from "+table+filter, args...)
if err != nil {
return nil, err
}
for rws.Next() {
var r string
if err := rws.Scan(&r); err != nil {
return nil, err
}
out = append(out, r)
}
return
}
var _ http.Handler = &handler{}