diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..847144b --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2020 The Decred developers + +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. \ No newline at end of file diff --git a/README.md b/README.md index 13c56cc..dbf2ca2 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,30 @@ # dcrvsp +[![Build Status](https://github.com/jholdstock/dcrvsp/workflows/Build%20and%20Test/badge.svg)](https://github.com/jholdstock/dcrvsp/actions) +[![ISC License](https://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![Go Report Card](https://goreportcard.com/badge/github.com/jholdstock/dcrvsp)](https://goreportcard.com/report/github.com/jholdstock/dcrvsp) + ## Design decisions -- [gin-gonic](https://github.com/gin-gonic/gin) webserver -- [bbolt](https://github.com/etcd-io/bbolt) database +- [gin-gonic](https://github.com/gin-gonic/gin) webserver for both front-end and API. + - API uses JSON encoded reqs/resps in HTTP body. +- [bbolt](https://github.com/etcd-io/bbolt) k/v database. - Tickets are stored in a single bucket, using ticket hash as the key and a json encoded representation of the ticket as the value. +- [wsrpc](https://github.com/jrick/wsrpc) for dcrwallet comms. ## MVP features -- VSP API "v3" as described in [dcrstakepool #574](https://github.com/decred/dcrstakepool/issues/574) -and implemented in [dcrstakepool #625](https://github.com/decred/dcrstakepool/pull/625) - - Request fee amount - - Request fee address - - Pay fee - - Set voting preferences +- VSP API as described in [dcrstakepool #574](https://github.com/decred/dcrstakepool/issues/574) + - Request fee amount (`GET /fee`) + - Request fee address (`POST /feeaddress`) + - Pay fee (`POST /payFee`) + - Ticket status (`POST /ticketstatus`) + - Set voting preferences (TBD) + - A minimal, static, web front-end providing pool stats and basic connection instructions. +- Fees have an expiry period. If the fee is not paid within this period, the + client must request a new fee. This enables the VSP to alter its fee rate. ## Future features @@ -25,3 +34,12 @@ and implemented in [dcrstakepool #625](https://github.com/decred/dcrstakepool/pu - Accountability for both client and server changes to voting preferences. - Consistency checking across connected wallets. - Validate votebits provided in PayFee request are valid per current agendas. + +## Issue Tracker + +The [integrated github issue tracker](https://github.com/jholdstock/dcrvsp/issues) +is used for this project. + +## License + +dcrvsp is licensed under the [copyfree](http://copyfree.org) ISC License. \ No newline at end of file diff --git a/database/ticket.go b/database/ticket.go index 59c2eb7..7416590 100644 --- a/database/ticket.go +++ b/database/ticket.go @@ -54,7 +54,10 @@ func (vdb *VspDatabase) InsertFeeAddressVotingKey(address, votingKey string, vot if err != nil { return err } - ticketBkt.Put(k, ticketBytes) + err = ticketBkt.Put(k, ticketBytes) + if err != nil { + return err + } } } diff --git a/log.go b/log.go index 41ace19..2e9cff0 100644 --- a/log.go +++ b/log.go @@ -38,8 +38,8 @@ var ( // application shutdown. logRotator *rotator.Rotator - vspLog = backendLog.Logger("VSP") - dbLog = backendLog.Logger("DB") + log = backendLog.Logger("VSP") + dbLog = backendLog.Logger(" DB") ) // Initialize package-global logger variables. @@ -49,8 +49,8 @@ func init() { // subsystemLoggers maps each subsystem identifier to its associated logger. var subsystemLoggers = map[string]slog.Logger{ - "VSP": vspLog, - "DB": dbLog, + "VSP": log, + " DB": dbLog, } // initLogRotator initializes the logging rotater to write logs to logFile and diff --git a/main.go b/main.go index 1fca91b..365d23d 100644 --- a/main.go +++ b/main.go @@ -1,33 +1,42 @@ package main import ( - "log" + "fmt" + "os" "github.com/jholdstock/dcrvsp/database" "github.com/jrick/wsrpc/v2" ) -var cfg *config - -var db *database.VspDatabase - -var nodeConnection *wsrpc.Client +var ( + cfg *config + db *database.VspDatabase + nodeConnection *wsrpc.Client +) func main() { var err error - cfg, err := loadConfig() + cfg, err = loadConfig() if err != nil { - log.Fatalf("config error: %v", err) + // Don't use logger here because it may not be initialised yet. + fmt.Fprintf(os.Stderr, "config error: %v", err) + os.Exit(1) } db, err = database.New(cfg.dbPath) if err != nil { - log.Fatalf("database error: %v", err) + log.Errorf("database error: %v", err) + os.Exit(1) } - defer db.Close() // Start HTTP server - log.Printf("Listening on %s", cfg.Listen) - log.Print(newRouter().Run(cfg.Listen)) + log.Infof("Listening on %s", cfg.Listen) + // TODO: Make releaseMode properly configurable. + releaseMode := false + err = newRouter(releaseMode).Run(cfg.Listen) + if err != nil { + log.Errorf("web server terminated with error: %v", err) + os.Exit(1) + } } diff --git a/methods.go b/methods.go index c3c9167..542b8b6 100644 --- a/methods.go +++ b/methods.go @@ -9,7 +9,6 @@ import ( "encoding/json" "errors" "fmt" - "log" "net/http" "time" @@ -28,10 +27,10 @@ const ( defaultFeeAddressExpiration = 24 * time.Hour ) -func sendJSONResponse(resp interface{}, code int, c *gin.Context) { +func sendJSONResponse(resp interface{}, c *gin.Context) { dec, err := json.Marshal(resp) if err != nil { - log.Printf("JSON marshal error: %v", err) + log.Errorf("JSON marshal error: %v", err) c.AbortWithStatus(http.StatusInternalServerError) return } @@ -39,7 +38,7 @@ func sendJSONResponse(resp interface{}, code int, c *gin.Context) { sig := ed25519.Sign(cfg.signKey, dec) c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8") c.Writer.Header().Set("VSP-Signature", hex.EncodeToString(sig)) - c.Writer.WriteHeader(code) + c.Writer.WriteHeader(http.StatusOK) c.Writer.Write(dec) } @@ -47,14 +46,14 @@ func pubKey(c *gin.Context) { sendJSONResponse(pubKeyResponse{ Timestamp: time.Now().Unix(), PubKey: cfg.pubKey, - }, http.StatusOK, c) + }, c) } func fee(c *gin.Context) { sendJSONResponse(feeResponse{ Timestamp: time.Now().Unix(), Fee: cfg.VSPFee, - }, http.StatusOK, c) + }, c) } func feeAddress(c *gin.Context) { @@ -173,7 +172,7 @@ func feeAddress(c *gin.Context) { Request: feeAddressRequest, FeeAddress: newAddress, Expiration: now.Add(defaultFeeAddressExpiration).Unix(), - }, http.StatusOK, c) + }, c) } func payFee(c *gin.Context) { @@ -206,7 +205,7 @@ func payFee(c *gin.Context) { validFeeAddrs, err := db.GetInactiveFeeAddresses() if err != nil { - log.Fatalf("database error: %v", err) + log.Errorf("database error: %v", err) c.AbortWithError(http.StatusInternalServerError, errors.New("database error")) return } @@ -307,7 +306,7 @@ findAddress: Timestamp: now.Unix(), TxHash: resp, Request: payFeeRequest, - }, http.StatusOK, c) + }, c) } // PayFee2 is copied from the stakepoold implementation in #625 @@ -403,5 +402,5 @@ func ticketStatus(c *gin.Context) { Request: ticketStatusRequest, Status: "active", // TODO - active, pending, expired (missed, revoked?) VoteBits: voteBits, - }, http.StatusOK, c) + }, c) } diff --git a/params.go b/params.go index 34273cf..52a977c 100644 --- a/params.go +++ b/params.go @@ -6,24 +6,20 @@ import ( type netParams struct { *chaincfg.Params - DcrdRPCServerPort string WalletRPCServerPort string } var mainNetParams = netParams{ Params: chaincfg.MainNetParams(), - DcrdRPCServerPort: "9109", - WalletRPCServerPort: "9111", + WalletRPCServerPort: "9110", } var testNet3Params = netParams{ Params: chaincfg.TestNet3Params(), - DcrdRPCServerPort: "19109", - WalletRPCServerPort: "19111", + WalletRPCServerPort: "19110", } var simNetParams = netParams{ Params: chaincfg.SimNetParams(), - DcrdRPCServerPort: "19556", - WalletRPCServerPort: "19558", + WalletRPCServerPort: "19557", } diff --git a/public/css/dcrvsp.css b/public/css/dcrvsp.css new file mode 100644 index 0000000..f8732f2 --- /dev/null +++ b/public/css/dcrvsp.css @@ -0,0 +1,7 @@ +body { + background-color: #091440; + color: white; +} +img { + height: 70px; +} \ No newline at end of file diff --git a/public/images/decred-logo.svg b/public/images/decred-logo.svg new file mode 100644 index 0000000..e5bc1de --- /dev/null +++ b/public/images/decred-logo.svg @@ -0,0 +1 @@ +transparent background - primary - negative - gardient \ No newline at end of file diff --git a/router.go b/router.go index ab520d0..d76ff87 100644 --- a/router.go +++ b/router.go @@ -1,22 +1,51 @@ package main import ( + "net/http" + "github.com/gin-gonic/gin" ) -func newRouter() *gin.Engine { - router := gin.Default() +func newRouter(releaseMode bool) *gin.Engine { + // With release mode enabled, gin will only read template files once and cache them. + // With release mode disabled, templates will be reloaded on the fly. + if releaseMode { + gin.SetMode(gin.ReleaseMode) + } + + router := gin.New() + router.LoadHTMLGlob("templates/*") + + // Recovery middleware handles any go panics generated while processing web + // requests. Ensures a 500 response is sent to the client rather than + // sending no response at all. + router.Use(gin.Recovery()) + + if !releaseMode { + // Logger middleware outputs very detailed logging of webserver requests + // to the terminal. Does not get logged to file. + router.Use(gin.Logger()) + } + + // Serve static web resources + router.Static("/public", "./public/") + + router.GET("/", homepage) api := router.Group("/api") - - api.Use() { - router.GET("/fee", fee) - router.POST("/feeaddress", feeAddress) - router.GET("/pubkey", pubKey) - router.POST("/payfee", payFee) - router.POST("/ticketstatus", ticketStatus) + api.GET("/fee", fee) + api.POST("/feeaddress", feeAddress) + api.GET("/pubkey", pubKey) + api.POST("/payfee", payFee) + api.POST("/ticketstatus", ticketStatus) } return router } + +func homepage(c *gin.Context) { + c.HTML(http.StatusOK, "homepage.html", gin.H{ + "Message": "Welcome to dcrvsp!", + }) +} diff --git a/run_tests.sh b/run_tests.sh index a9c092a..6f285ee 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -27,8 +27,8 @@ golangci-lint run --disable-all --deadline=10m \ --enable=structcheck \ --enable=goimports \ --enable=misspell \ + --enable=unparam \ --enable=asciicheck -# --enable=unparam \ # --enable=deadcode \ # --enable=errcheck \ # --enable=unused \ \ No newline at end of file diff --git a/templates/homepage.html b/templates/homepage.html new file mode 100644 index 0000000..e9eaec2 --- /dev/null +++ b/templates/homepage.html @@ -0,0 +1,15 @@ + + + + + + + + dcrwages + + + + +

{{ .Message }}

+ + \ No newline at end of file