Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin page #119

Merged
merged 2 commits into from
Jun 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type config struct {
SupportEmail string `long:"supportemail" ini-name:"supportemail" description:"Email address for users in need of support."`
BackupInterval time.Duration `long:"backupinterval" ini-name:"backupinterval" description:"Time period between automatic database backups. Valid time units are {s,m,h}. Minimum 30 seconds."`
VspClosed bool `long:"vspclosed" ini-name:"vspclosed" description:"Closed prevents the VSP from accepting new tickets."`
AdminPass string `long:"adminpass" ini-name:"adminpass" description:"Password for accessing admin page."`

// The following flags should be set on CLI only, not via config file.
FeeXPub string `long:"feexpub" no-ini:"true" description:"Cold wallet xpub used for collecting fees. Should be provided once to initialize a vspd database."`
Expand Down Expand Up @@ -271,6 +272,11 @@ func loadConfig() (*config, error) {
return nil, errors.New("the supportemail option is not set")
}

// Ensure the administrator password is set.
if cfg.AdminPass == "" {
return nil, errors.New("the adminpass option is not set")
}

// Ensure the dcrd RPC username is set.
if cfg.DcrdUser == "" {
return nil, errors.New("the dcrduser option is not set")
Expand Down
27 changes: 27 additions & 0 deletions database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ var (
versionK = []byte("version")
// feeXPub is the extended public key used for collecting VSP fees.
feeXPubK = []byte("feeXPub")
// cookieSecret is the secret key for initializing the cookie store.
cookieSecretK = []byte("cookieSecret")
// privatekey is the private key.
privateKeyK = []byte("privatekey")
// lastaddressindex is the index of the last address used for fees.
Expand Down Expand Up @@ -103,6 +105,18 @@ func CreateNew(dbFile, feeXPub string) error {
return err
}

// Generate a secret key for initializing the cookie store.
log.Info("Generating cookie secret")
secret := make([]byte, 32)
_, err = rand.Read(secret)
if err != nil {
return err
}
err = vspBkt.Put(cookieSecretK, secret)
if err != nil {
return err
}

log.Info("Storing extended public key")
// Store fee xpub
err = vspBkt.Put(feeXPubK, []byte(feeXPub))
Expand Down Expand Up @@ -228,3 +242,16 @@ func (vdb *VspDatabase) GetFeeXPub() (string, error) {

return feeXPub, err
}

func (vdb *VspDatabase) GetCookieSecret() ([]byte, error) {
var cookieSecret []byte
err := vdb.db.View(func(tx *bolt.Tx) error {
vspBkt := tx.Bucket(vspBktK)

cookieSecret = vspBkt.Get(cookieSecretK)

return nil
})

return cookieSecret, err
}
2 changes: 1 addition & 1 deletion database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ func TestDatabase(t *testing.T) {
}

// TODO: Add tests for CountTickets, GetUnconfirmedTickets, GetPendingFees,
// GetUnconfirmedFees.
// GetUnconfirmedFees, GetAllTickets.
21 changes: 21 additions & 0 deletions database/ticket.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,24 @@ func (vdb *VspDatabase) GetUnconfirmedFees() ([]Ticket, error) {

return tickets, err
}

func (vdb *VspDatabase) GetAllTickets() ([]Ticket, error) {
var tickets []Ticket
err := vdb.db.View(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)

return ticketBkt.ForEach(func(k, v []byte) error {
var ticket Ticket
err := json.Unmarshal(v, &ticket)
if err != nil {
return fmt.Errorf("could not unmarshal ticket: %v", err)
}

tickets = append(tickets, ticket)

return nil
})
})

return tickets, err
}
11 changes: 6 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ go 1.13

require (
decred.org/dcrwallet v1.2.3-0.20200519180100-f1aa4c354e05
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200528191943-a2771b8ce693
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200528191943-a2771b8ce693
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200607041702-62fa0661bd57
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200607041702-62fa0661bd57
github.com/decred/dcrd/dcrec v1.0.0
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200528191943-a2771b8ce693
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200528191943-a2771b8ce693
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200607041702-62fa0661bd57
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200607041702-62fa0661bd57
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.1-0.20200527025017-6fc98347d984
github.com/decred/dcrd/txscript/v3 v3.0.0-20200528191943-a2771b8ce693
github.com/decred/dcrd/txscript/v3 v3.0.0-20200607041702-62fa0661bd57
github.com/decred/dcrd/wire v1.3.0
github.com/decred/slog v1.0.0
github.com/gin-gonic/gin v1.6.3
github.com/gorilla/sessions v1.2.0
github.com/jessevdk/go-flags v1.4.0
github.com/jrick/bitset v1.0.0
github.com/jrick/logrotate v1.0.0
Expand Down
24 changes: 14 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ github.com/decred/dcrd/addrmgr v1.1.0/go.mod h1:exghL+0+QeVvO4MXezWJ1C2tcpBn3ngf
github.com/decred/dcrd/blockchain/stake/v2 v2.0.2/go.mod h1:o2TT/l/YFdrt15waUdlZ3g90zfSwlA0WgQqHV9UGJF4=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:aDL94kcVJfaaJP+acWUJrlK7g7xEOqTSiFe6bSN3yRQ=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200528191943-a2771b8ce693 h1:2Wbj27h04yq4l8KSqCJkZQnmWiKfUI4kNYaUms5oSnY=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200607041702-62fa0661bd57 h1:4/glIgVrylnAAYlpfgBmxg8dX9DHWJu/tlBagcNiXz4=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU=
github.com/decred/dcrd/blockchain/standalone v1.1.0 h1:yclvVGEY09Gf8A4GSAo+NCtL1dW2TYJ4OKp4+g0ICI0=
github.com/decred/dcrd/blockchain/standalone v1.1.0/go.mod h1:6K8ZgzlWM1Kz2TwXbrtiAvfvIwfAmlzrtpA7CVPCUPE=
github.com/decred/dcrd/blockchain/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:R9rIXU8kEJVC9Z4LAlh9bo9hiT3a+ihys3mCrz4PVao=
Expand All @@ -34,8 +34,8 @@ github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215015031-3283587e6add/go.mod h1:
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200528191943-a2771b8ce693 h1:zTb6LJaZpYAWItJbo/XY8howiwCceEfWzHZbI06PRNI=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:OHbKBa6UZZOXCU1Y8f9Ta3O+GShto7nB1O0nuEutKq4=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200607041702-62fa0661bd57 h1:agpmzwuPv/iaePlk/2MxXPU6Zz14qy2LY1kWmRCBdAQ=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:OHbKBa6UZZOXCU1Y8f9Ta3O+GShto7nB1O0nuEutKq4=
github.com/decred/dcrd/connmgr/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:mvIMJsrOEngogmVrq+tdbPIZchHVgGnVBZeNwj1cW6E=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
Expand All @@ -61,23 +61,23 @@ github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215015031-3283587e6add/go.mod h1:C
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:48ZLpNNrRIYfqYxmvzMgOZrnTZUU3aTJveWtamCkOxo=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:jFxEd2LWDLvrWlrIiyx9ZGTQjvoFHZ0OVfBdyIX7jSw=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:/CDBC1SOXKrmihavgXviaTr6eVZSAWKQqEbRmacDxgg=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200528191943-a2771b8ce693 h1:6Sz3pijZe2cyY8y1hhTmeQDz6Bg+KheIA5Hkks1n8QY=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:85NtF/fmqL2UDf0/gLhTHG/m/0HQHwG+erQKkwWW27A=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200607041702-62fa0661bd57 h1://D7EuW5XY8Me8uOHH2PH9APsdUQ0cm3KIyXXIcMYGc=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:85NtF/fmqL2UDf0/gLhTHG/m/0HQHwG+erQKkwWW27A=
github.com/decred/dcrd/gcs/v2 v2.0.0/go.mod h1:3XjKcrtvB+r2ezhIsyNCLk6dRnXRJVyYmsd1P3SkU3o=
github.com/decred/dcrd/gcs/v2 v2.0.2-0.20200312171759-0a8cc56a776e h1:tBOk2P8F9JyRUSp0iRTs4nYEBro1FKBDIbg/UualLWw=
github.com/decred/dcrd/gcs/v2 v2.0.2-0.20200312171759-0a8cc56a776e/go.mod h1:JJGd1m0DrFgV4J2J8HKNB9YVkM06ewQHT6iINis39Z4=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200528191943-a2771b8ce693 h1:8MKtyq4+zxRC/SEtJuF4+qB+Svd5vvHZeG9kbHxg9wY=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200607041702-62fa0661bd57 h1:fGahtE/RfIBYlguw7tG11g1RBMBqqaZKVWFYlccYbX4=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM=
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0/go.mod h1:c5S+PtQWNIA2aUakgrLhrlopkMadcOv51dWhCEdo49c=
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.1-0.20200527025017-6fc98347d984 h1:xfdiilBsDinOLbglqzHH98fOO1iBtVrpSHMVpNK/2lg=
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.1-0.20200527025017-6fc98347d984/go.mod h1:c5S+PtQWNIA2aUakgrLhrlopkMadcOv51dWhCEdo49c=
github.com/decred/dcrd/txscript/v2 v2.1.0/go.mod h1:XaJAVrZU4NWRx4UEzTiDAs86op1m8GRJLz24SDBKOi0=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:ATMA8K0SOo+M9Wdbr6dMnAd8qICJi6pXjGLlKsJc99E=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:KsDS7McU1yFaCYR9LCIwk6YnE15YN3wJUDxhKdFqlsc=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200528191943-a2771b8ce693 h1:8QJvuZnGXKs1x2VftugOOApMyARYH4tnu4n4o+JASZo=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200607041702-62fa0661bd57 h1:UaIGTrn1bpdZVgULdkwxZhULw5T5Jm3+OPJDw4+/p5c=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
github.com/decred/dcrd/wire v1.3.0 h1:X76I2/a8esUmxXmFpJpAvXEi014IA4twgwcOBeIS8lE=
github.com/decred/dcrd/wire v1.3.0/go.mod h1:fnKGlUY2IBuqnpxx5dYRU5Oiq392OBqAuVjRVSkIoXM=
github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0=
Expand Down Expand Up @@ -105,6 +105,10 @@ github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
Expand Down
1 change: 1 addition & 0 deletions harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ webserverdebug = false
supportemail = example@test.com
backupinterval = 3m0s
vspclosed = false
adminpass=12345
EOF

tmux new-window -t $TMUX_SESSION -n "vspd"
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func run(ctx context.Context) error {
NetParams: cfg.netParams.Params,
SupportEmail: cfg.SupportEmail,
VspClosed: cfg.VspClosed,
AdminPass: cfg.AdminPass,
}
err = webapi.Start(ctx, shutdownRequestChannel, &shutdownWg, cfg.Listen, db,
dcrd, wallets, cfg.WebServerDebug, apiCfg)
Expand Down
69 changes: 69 additions & 0 deletions webapi/admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package webapi

import (
"net/http"

"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
)

// adminPage is the handler for "GET /admin". The admin template will be
// rendered if the current session is authenticated as an admin, otherwise the
// login template will be rendered.
func adminPage(c *gin.Context) {
session := c.MustGet("session").(*sessions.Session)
admin := session.Values["admin"]

if admin == nil {
c.HTML(http.StatusUnauthorized, "login.html", gin.H{})
return
}

tickets, err := db.GetAllTickets()
if err != nil {
log.Errorf("GetAllTickets error: %v", err)
c.String(http.StatusInternalServerError, "Error getting tickets from db")
return
}

c.HTML(http.StatusOK, "admin.html", gin.H{
"Tickets": tickets,
})
}

// adminLogin is the handler for "POST /admin". If a valid password is provided,
// the current session will be authenticated as an admin.
func adminLogin(c *gin.Context) {
password := c.PostForm("password")

if password != cfg.AdminPass {
log.Warnf("Failed login attempt from %s", c.ClientIP())
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"IncorrectPassword": true,
})
return
}

setAdminStatus(true, c)
}

// adminLogout is the handler for "POST /admin/logout". The current session will
// have its admin authentication removed.
func adminLogout(c *gin.Context) {
setAdminStatus(nil, c)
}

// setAdminStatus stores the authentication status of the current session.
func setAdminStatus(admin interface{}, c *gin.Context) {
session := c.MustGet("session").(*sessions.Session)
session.Values["admin"] = admin
err := session.Save(c.Request, c.Writer)
if err != nil {
log.Errorf("Error saving session: %v", err)
c.String(http.StatusInternalServerError, "Error saving session")
return
}

c.Redirect(http.StatusFound, "/admin")
c.Abort()
}
35 changes: 35 additions & 0 deletions webapi/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,51 @@ package webapi

import (
"net/http"
"strings"

"github.com/decred/vspd/rpc"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gorilla/sessions"
)

type ticketHashRequest struct {
TicketHash string `json:"tickethash" binding:"required"`
}

// withSession middleware adds a gorilla session to the request context for
// downstream handlers to make use of. Sessions are used by admin pages to
// maintain authentication status.
func withSession(store *sessions.CookieStore) gin.HandlerFunc {
return func(c *gin.Context) {
session, err := store.Get(c.Request, "vspd-session")
if err != nil {
// "value is not valid" occurs if the cookie secret changes. This is
// common during development (eg. when using the test harness) but
// it should not occur in production.
if strings.Contains(err.Error(), "securecookie: the value is not valid") {
log.Warn("Cookie secret has changed. Generating new session.")

// Persist the newly generated session.
err = store.Save(c.Request, c.Writer, session)
if err != nil {
log.Errorf("Error saving session: %v", err)
c.String(http.StatusInternalServerError, "Error saving session")
c.Abort()
return
}
} else {
log.Errorf("Session error: %v", err)
c.String(http.StatusInternalServerError, "Error getting session")
c.Abort()
return
}
}

c.Set("session", session)
}
}

// withDcrdClient middleware adds a dcrd client to the request
// context for downstream handlers to make use of.
func withDcrdClient() gin.HandlerFunc {
Expand Down
41 changes: 41 additions & 0 deletions webapi/templates/admin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{{ template "header" . }}

<form action="/admin/logout" method="post">
<button type="submit">Logout</button>
</form>

<table>
<tr>
<td>Hash</td>
<td>CommitmentAddress</td>
<td>FeeAddressIndex</td>
<td>FeeAddress</td>
<td>FeeAmount</td>
<td>FeeExpiration</td>
<td>Confirmed</td>
<td>VoteChoices</td>
<td>VotingWIF</td>
<td>FeeTxHex</td>
<td>FeeTxHash</td>
<td>FeeConfirmed</td>
</tr>
{{ range .Tickets }}
<tr>
<td>{{ printf "%.10s" .Hash }}...</td>
<td>{{ printf "%.10s" .CommitmentAddress }}...</td>
<td>{{ printf "%d" .FeeAddressIndex }}</td>
<td>{{ printf "%.10s" .FeeAddress }}...</td>
<td>{{ printf "%f" .FeeAmount }}</td>
<td>{{ printf "%d" .FeeExpiration }}</td>
<td>{{ printf "%t" .Confirmed }}</td>
<td>{{ printf "%.10s" .VoteChoices }}...</td>
<td>{{ printf "%.10s" .VotingWIF }}...</td>
<td>{{ printf "%.10s" .FeeTxHex }}...</td>
<td>{{ printf "%.10s" .FeeTxHash }}...</td>
<td>{{ printf "%t" .FeeConfirmed }}</td>
</tr>
{{ end }}
</table>

</body>
</html>
Loading