Use a waitgroup to wait for shutdown tasks (#21)
This commit is contained in:
parent
1092df5224
commit
6ca2f620b7
@ -154,6 +154,7 @@ func loadConfig() (*config, error) {
|
|||||||
// directory is updated, other variables need to be updated to
|
// directory is updated, other variables need to be updated to
|
||||||
// reflect the new changes.
|
// reflect the new changes.
|
||||||
if preCfg.HomeDir != "" {
|
if preCfg.HomeDir != "" {
|
||||||
|
cfg.HomeDir = cleanAndExpandPath(cfg.HomeDir)
|
||||||
cfg.HomeDir, _ = filepath.Abs(preCfg.HomeDir)
|
cfg.HomeDir, _ = filepath.Abs(preCfg.HomeDir)
|
||||||
|
|
||||||
if preCfg.ConfigFile == defaultConfigFile {
|
if preCfg.ConfigFile == defaultConfigFile {
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
@ -25,20 +27,37 @@ var (
|
|||||||
versionK = []byte("version")
|
versionK = []byte("version")
|
||||||
)
|
)
|
||||||
|
|
||||||
// New initialises and returns a database. If no database file is found at the
|
// Open initialises and returns an open database. If no database file is found
|
||||||
// provided path, a new one will be created. Returns an open database which
|
// at the provided path, a new one will be created.
|
||||||
// should always be closed after use.
|
func Open(ctx context.Context, shutdownWg *sync.WaitGroup, dbFile string) (*VspDatabase, error) {
|
||||||
func New(dbFile string) (*VspDatabase, error) {
|
|
||||||
db, err := bolt.Open(dbFile, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
db, err := bolt.Open(dbFile, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to open db file: %v", err)
|
return nil, fmt.Errorf("unable to open db file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debugf("Opened database file %s", dbFile)
|
||||||
|
|
||||||
|
// Add the graceful shutdown to the waitgroup.
|
||||||
|
shutdownWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
// Wait until shutdown is signaled before shutting down.
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
log.Debug("Closing database...")
|
||||||
|
err := db.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error closing database: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Debug("Database closed")
|
||||||
|
}
|
||||||
|
shutdownWg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
// Create all storage buckets of the VSP if they don't already exist.
|
// Create all storage buckets of the VSP if they don't already exist.
|
||||||
var newDB bool
|
|
||||||
err = db.Update(func(tx *bolt.Tx) error {
|
err = db.Update(func(tx *bolt.Tx) error {
|
||||||
if tx.Bucket(vspBktK) == nil {
|
if tx.Bucket(vspBktK) == nil {
|
||||||
newDB = true
|
log.Debug("Initialising new database")
|
||||||
// Create parent bucket.
|
// Create parent bucket.
|
||||||
vspBkt, err := tx.CreateBucket(vspBktK)
|
vspBkt, err := tx.CreateBucket(vspBktK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -67,17 +86,5 @@ func New(dbFile string) (*VspDatabase, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if newDB {
|
|
||||||
log.Debugf("Created new database %s", dbFile)
|
|
||||||
} else {
|
|
||||||
log.Debugf("Using existing database %s", dbFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &VspDatabase{db: db}, nil
|
return &VspDatabase{db: db}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close releases all database resources. It will block waiting for any open
|
|
||||||
// transactions to finish before closing the database and returning.
|
|
||||||
func (vdb *VspDatabase) Close() error {
|
|
||||||
return vdb.db.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,7 +40,9 @@ func TestDatabase(t *testing.T) {
|
|||||||
for testName, test := range tests {
|
for testName, test := range tests {
|
||||||
// Create a new blank database for each sub-test.
|
// Create a new blank database for each sub-test.
|
||||||
var err error
|
var err error
|
||||||
db, err = New(testDb)
|
var wg sync.WaitGroup
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
db, err = Open(ctx, &wg, testDb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error creating test database: %v", err)
|
t.Fatalf("error creating test database: %v", err)
|
||||||
}
|
}
|
||||||
@ -46,8 +50,10 @@ func TestDatabase(t *testing.T) {
|
|||||||
// Run the sub-test.
|
// Run the sub-test.
|
||||||
t.Run(testName, test)
|
t.Run(testName, test)
|
||||||
|
|
||||||
// Close and remove test database after each sub-test.
|
// Request database shutdown and wait for it to complete.
|
||||||
db.Close()
|
cancel()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
os.Remove(testDb)
|
os.Remove(testDb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
log.go
4
log.go
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/jrick/logrotate/rotator"
|
"github.com/jrick/logrotate/rotator"
|
||||||
|
|
||||||
"github.com/jholdstock/dcrvsp/database"
|
"github.com/jholdstock/dcrvsp/database"
|
||||||
|
"github.com/jholdstock/dcrvsp/webapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
// logWriter implements an io.Writer that outputs to both standard output and
|
// logWriter implements an io.Writer that outputs to both standard output and
|
||||||
@ -40,17 +41,20 @@ var (
|
|||||||
|
|
||||||
log = backendLog.Logger("VSP")
|
log = backendLog.Logger("VSP")
|
||||||
dbLog = backendLog.Logger(" DB")
|
dbLog = backendLog.Logger(" DB")
|
||||||
|
apiLog = backendLog.Logger("API")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize package-global logger variables.
|
// Initialize package-global logger variables.
|
||||||
func init() {
|
func init() {
|
||||||
database.UseLogger(dbLog)
|
database.UseLogger(dbLog)
|
||||||
|
webapi.UseLogger(apiLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
// subsystemLoggers maps each subsystem identifier to its associated logger.
|
// subsystemLoggers maps each subsystem identifier to its associated logger.
|
||||||
var subsystemLoggers = map[string]slog.Logger{
|
var subsystemLoggers = map[string]slog.Logger{
|
||||||
"VSP": log,
|
"VSP": log,
|
||||||
" DB": dbLog,
|
" DB": dbLog,
|
||||||
|
"API": apiLog,
|
||||||
}
|
}
|
||||||
|
|
||||||
// initLogRotator initializes the logging rotater to write logs to logFile and
|
// initLogRotator initializes the logging rotater to write logs to logFile and
|
||||||
|
|||||||
95
main.go
95
main.go
@ -4,21 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"sync"
|
||||||
|
|
||||||
"github.com/jholdstock/dcrvsp/database"
|
"github.com/jholdstock/dcrvsp/database"
|
||||||
|
"github.com/jholdstock/dcrvsp/webapi"
|
||||||
"github.com/jrick/wsrpc/v2"
|
"github.com/jrick/wsrpc/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
cfg *config
|
|
||||||
db *database.VspDatabase
|
|
||||||
nodeConnection *wsrpc.Client
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Create a context that is cancelled when a shutdown request is received
|
// Create a context that is cancelled when a shutdown request is received
|
||||||
// through an interrupt signal.
|
// through an interrupt signal.
|
||||||
@ -36,78 +29,50 @@ func main() {
|
|||||||
// opening the database, starting the webserver, and stopping all started
|
// opening the database, starting the webserver, and stopping all started
|
||||||
// services when the context is cancelled.
|
// services when the context is cancelled.
|
||||||
func run(ctx context.Context) error {
|
func run(ctx context.Context) error {
|
||||||
var err error
|
|
||||||
|
|
||||||
// Load config file and parse CLI args.
|
// Load config file and parse CLI args.
|
||||||
cfg, err = loadConfig()
|
cfg, err := loadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't use logger here because it may not be initialised.
|
// Don't use logger here because it may not be initialised.
|
||||||
fmt.Fprintf(os.Stderr, "Config error: %v", err)
|
fmt.Fprintf(os.Stderr, "Config error: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Waitgroup for services to signal when they have shutdown cleanly.
|
||||||
|
var shutdownWg sync.WaitGroup
|
||||||
|
|
||||||
// Open database.
|
// Open database.
|
||||||
db, err = database.New(cfg.dbPath)
|
db, err := database.Open(ctx, &shutdownWg, cfg.dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Database error: %v", err)
|
log.Errorf("Database error: %v", err)
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Close database.
|
|
||||||
defer func() {
|
|
||||||
log.Debug("Closing database...")
|
|
||||||
err := db.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error closing database: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Debug("Database closed")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Create TCP listener for webserver.
|
|
||||||
var listenConfig net.ListenConfig
|
|
||||||
listener, err := listenConfig.Listen(ctx, "tcp", cfg.Listen)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Failed to create tcp listener: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Infof("Listening on %s", cfg.Listen)
|
|
||||||
|
|
||||||
// Create webserver.
|
|
||||||
// TODO: Make releaseMode properly configurable.
|
|
||||||
releaseMode := true
|
|
||||||
srv := http.Server{
|
|
||||||
Handler: newRouter(releaseMode),
|
|
||||||
ReadTimeout: 5 * time.Second, // slow requests should not hold connections opened
|
|
||||||
WriteTimeout: 60 * time.Second, // hung responses must die
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start webserver.
|
|
||||||
go func() {
|
|
||||||
err = srv.Serve(listener)
|
|
||||||
// If the server dies for any reason other than ErrServerClosed (from
|
|
||||||
// graceful server.Shutdown), log the error and request dcrvsp be
|
|
||||||
// shutdown.
|
|
||||||
if err != nil && err != http.ErrServerClosed {
|
|
||||||
log.Errorf("Unexpected webserver error: %v", err)
|
|
||||||
requestShutdown()
|
requestShutdown()
|
||||||
|
shutdownWg.Wait()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
// Stop webserver.
|
// TODO: Create real RPC client.
|
||||||
defer func() {
|
var rpc *wsrpc.Client
|
||||||
log.Debug("Stopping webserver...")
|
|
||||||
// Give the webserver 5 seconds to finish what it is doing.
|
// Create and start webapi server.
|
||||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
apiCfg := webapi.Config{
|
||||||
defer cancel()
|
SignKey: cfg.signKey,
|
||||||
if err := srv.Shutdown(timeoutCtx); err != nil {
|
PubKey: cfg.pubKey,
|
||||||
log.Errorf("Failed to stop webserver cleanly: %v", err)
|
VSPFee: cfg.VSPFee,
|
||||||
|
NetParams: cfg.netParams.Params,
|
||||||
|
}
|
||||||
|
// TODO: Make releaseMode properly configurable. Release mode enables very
|
||||||
|
// detailed webserver logging and live reloading of HTML templates.
|
||||||
|
releaseMode := true
|
||||||
|
err = webapi.Start(ctx, shutdownRequestChannel, &shutdownWg, cfg.Listen, db, rpc, releaseMode, apiCfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to initialise webapi: %v", err)
|
||||||
|
requestShutdown()
|
||||||
|
shutdownWg.Wait()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
log.Debug("Webserver stopped")
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait until shutdown is signaled before returning and running deferred
|
// Wait for shutdown tasks to complete before returning.
|
||||||
// shutdown tasks.
|
shutdownWg.Wait()
|
||||||
<-ctx.Done()
|
|
||||||
|
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|||||||
52
router.go
52
router.go
@ -1,52 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
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.GET("/fee", fee)
|
|
||||||
api.POST("/feeaddress", feeAddress)
|
|
||||||
api.GET("/pubkey", pubKey)
|
|
||||||
api.POST("/payfee", payFee)
|
|
||||||
api.POST("/setvotebits", setVoteBits)
|
|
||||||
api.POST("/ticketstatus", ticketStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
return router
|
|
||||||
}
|
|
||||||
|
|
||||||
func homepage(c *gin.Context) {
|
|
||||||
c.HTML(http.StatusOK, "homepage.html", gin.H{
|
|
||||||
"Message": "Welcome to dcrvsp!",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -28,7 +28,7 @@ golangci-lint run --disable-all --deadline=10m \
|
|||||||
--enable=goimports \
|
--enable=goimports \
|
||||||
--enable=misspell \
|
--enable=misspell \
|
||||||
--enable=unparam \
|
--enable=unparam \
|
||||||
|
--enable=deadcode \
|
||||||
|
--enable=unused \
|
||||||
--enable=asciicheck
|
--enable=asciicheck
|
||||||
# --enable=deadcode \
|
|
||||||
# --enable=errcheck \
|
# --enable=errcheck \
|
||||||
# --enable=unused \
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package webapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/decred/dcrd/chaincfg/v3"
|
"github.com/decred/dcrd/chaincfg/v3"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package webapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
26
webapi/log.go
Normal file
26
webapi/log.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package webapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/decred/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// log is a logger that is initialized with no output filters. This
|
||||||
|
// means the package will not perform any logging by default until the caller
|
||||||
|
// requests it.
|
||||||
|
var log slog.Logger
|
||||||
|
|
||||||
|
// The default amount of logging is none.
|
||||||
|
func init() {
|
||||||
|
DisableLog()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableLog disables all library log output. Logging output is disabled
|
||||||
|
// by default until UseLogger is called.
|
||||||
|
func DisableLog() {
|
||||||
|
log = slog.Disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseLogger uses a specified Logger to output package logging info.
|
||||||
|
func UseLogger(logger slog.Logger) {
|
||||||
|
log = logger
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package webapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -36,7 +36,7 @@ func sendJSONResponse(resp interface{}, c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sig := ed25519.Sign(cfg.signKey, dec)
|
sig := ed25519.Sign(cfg.SignKey, dec)
|
||||||
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
c.Writer.Header().Set("VSP-Signature", hex.EncodeToString(sig))
|
c.Writer.Header().Set("VSP-Signature", hex.EncodeToString(sig))
|
||||||
c.Writer.WriteHeader(http.StatusOK)
|
c.Writer.WriteHeader(http.StatusOK)
|
||||||
@ -46,7 +46,7 @@ func sendJSONResponse(resp interface{}, c *gin.Context) {
|
|||||||
func pubKey(c *gin.Context) {
|
func pubKey(c *gin.Context) {
|
||||||
sendJSONResponse(pubKeyResponse{
|
sendJSONResponse(pubKeyResponse{
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
PubKey: cfg.pubKey,
|
PubKey: cfg.PubKey,
|
||||||
}, c)
|
}, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ func feeAddress(c *gin.Context) {
|
|||||||
c.AbortWithError(http.StatusBadRequest, errors.New("transaction does not have minimum confirmations"))
|
c.AbortWithError(http.StatusBadRequest, errors.New("transaction does not have minimum confirmations"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if resp.Confirmations > int64(uint32(cfg.netParams.TicketMaturity)+cfg.netParams.TicketExpiry) {
|
if resp.Confirmations > int64(uint32(cfg.NetParams.TicketMaturity)+cfg.NetParams.TicketExpiry) {
|
||||||
c.AbortWithError(http.StatusBadRequest, errors.New("transaction too old"))
|
c.AbortWithError(http.StatusBadRequest, errors.New("transaction too old"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -147,7 +147,7 @@ func feeAddress(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get commitment address
|
// Get commitment address
|
||||||
addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, cfg.netParams)
|
addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, cfg.NetParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, errors.New("failed to get commitment address"))
|
c.AbortWithError(http.StatusInternalServerError, errors.New("failed to get commitment address"))
|
||||||
return
|
return
|
||||||
@ -155,7 +155,7 @@ func feeAddress(c *gin.Context) {
|
|||||||
|
|
||||||
// verify message
|
// verify message
|
||||||
message := fmt.Sprintf("vsp v3 getfeeaddress %s", msgTx.TxHash())
|
message := fmt.Sprintf("vsp v3 getfeeaddress %s", msgTx.TxHash())
|
||||||
err = dcrutil.VerifyMessage(addr.Address(), signature, message, cfg.netParams.Params)
|
err = dcrutil.VerifyMessage(addr.Address(), signature, message, cfg.NetParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusBadRequest, errors.New("invalid signature"))
|
c.AbortWithError(http.StatusBadRequest, errors.New("invalid signature"))
|
||||||
return
|
return
|
||||||
@ -219,7 +219,7 @@ func payFee(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
votingKey := payFeeRequest.VotingKey
|
votingKey := payFeeRequest.VotingKey
|
||||||
votingWIF, err := dcrutil.DecodeWIF(votingKey, cfg.netParams.PrivateKeyID)
|
votingWIF, err := dcrutil.DecodeWIF(votingKey, cfg.NetParams.PrivateKeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
@ -250,7 +250,7 @@ func payFee(c *gin.Context) {
|
|||||||
findAddress:
|
findAddress:
|
||||||
for _, txOut := range feeTx.TxOut {
|
for _, txOut := range feeTx.TxOut {
|
||||||
_, addresses, _, err := txscript.ExtractPkScriptAddrs(scriptVersion,
|
_, addresses, _, err := txscript.ExtractPkScriptAddrs(scriptVersion,
|
||||||
txOut.PkScript, cfg.netParams)
|
txOut.PkScript, cfg.NetParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Extract: %v", err)
|
fmt.Printf("Extract: %v", err)
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
@ -279,13 +279,13 @@ findAddress:
|
|||||||
c.AbortWithError(http.StatusInternalServerError, errors.New("database error"))
|
c.AbortWithError(http.StatusInternalServerError, errors.New("database error"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
voteAddr, err := dcrutil.DecodeAddress(feeEntry.CommitmentAddress, cfg.netParams)
|
voteAddr, err := dcrutil.DecodeAddress(feeEntry.CommitmentAddress, cfg.NetParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("PayFee: DecodeAddress: %v", err)
|
fmt.Printf("PayFee: DecodeAddress: %v", err)
|
||||||
c.AbortWithError(http.StatusInternalServerError, errors.New("database error"))
|
c.AbortWithError(http.StatusInternalServerError, errors.New("database error"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, err = dcrutil.NewAddressPubKeyHash(dcrutil.Hash160(votingWIF.PubKey()), cfg.netParams,
|
_, err = dcrutil.NewAddressPubKeyHash(dcrutil.Hash160(votingWIF.PubKey()), cfg.NetParams,
|
||||||
dcrec.STEcdsaSecp256k1)
|
dcrec.STEcdsaSecp256k1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("PayFee: NewAddressPubKeyHash: %v", err)
|
fmt.Printf("PayFee: NewAddressPubKeyHash: %v", err)
|
||||||
@ -305,7 +305,7 @@ findAddress:
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
minFee := txrules.StakePoolTicketFee(sDiff, relayFee, int32(feeEntry.BlockHeight), cfg.VSPFee, cfg.netParams.Params)
|
minFee := txrules.StakePoolTicketFee(sDiff, relayFee, int32(feeEntry.BlockHeight), cfg.VSPFee, cfg.NetParams)
|
||||||
if feeAmount < minFee {
|
if feeAmount < minFee {
|
||||||
fmt.Printf("too cheap: %v %v", feeAmount, minFee)
|
fmt.Printf("too cheap: %v %v", feeAmount, minFee)
|
||||||
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("dont get cheap on me, dodgson (sent:%v required:%v)", feeAmount, minFee))
|
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("dont get cheap on me, dodgson (sent:%v required:%v)", feeAmount, minFee))
|
||||||
@ -411,7 +411,7 @@ func setVoteBits(c *gin.Context) {
|
|||||||
|
|
||||||
// votebits
|
// votebits
|
||||||
voteBits := setVoteBitsRequest.VoteBits
|
voteBits := setVoteBitsRequest.VoteBits
|
||||||
if !isValidVoteBits(cfg.netParams.Params, currentVoteVersion(cfg.netParams.Params), voteBits) {
|
if !isValidVoteBits(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteBits) {
|
||||||
c.AbortWithError(http.StatusBadRequest, errors.New("invalid votebits"))
|
c.AbortWithError(http.StatusBadRequest, errors.New("invalid votebits"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -424,7 +424,7 @@ func setVoteBits(c *gin.Context) {
|
|||||||
|
|
||||||
// verify message
|
// verify message
|
||||||
message := fmt.Sprintf("vsp v3 setvotebits %d %s %d", setVoteBitsRequest.Timestamp, txHash, voteBits)
|
message := fmt.Sprintf("vsp v3 setvotebits %d %s %d", setVoteBitsRequest.Timestamp, txHash, voteBits)
|
||||||
err = dcrutil.VerifyMessage(addr, signature, message, cfg.netParams.Params)
|
err = dcrutil.VerifyMessage(addr, signature, message, cfg.NetParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusBadRequest, errors.New("message did not pass verification"))
|
c.AbortWithError(http.StatusBadRequest, errors.New("message did not pass verification"))
|
||||||
return
|
return
|
||||||
@ -478,7 +478,7 @@ func ticketStatus(c *gin.Context) {
|
|||||||
|
|
||||||
// verify message
|
// verify message
|
||||||
message := fmt.Sprintf("vsp v3 ticketstatus %d %s", ticketStatusRequest.Timestamp, ticketHashStr)
|
message := fmt.Sprintf("vsp v3 ticketstatus %d %s", ticketStatusRequest.Timestamp, ticketHashStr)
|
||||||
err = dcrutil.VerifyMessage(addr, signature, message, cfg.netParams.Params)
|
err = dcrutil.VerifyMessage(addr, signature, message, cfg.NetParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusBadRequest, errors.New("invalid signature"))
|
c.AbortWithError(http.StatusBadRequest, errors.New("invalid signature"))
|
||||||
return
|
return
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package webapi
|
||||||
|
|
||||||
type pubKeyResponse struct {
|
type pubKeyResponse struct {
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
123
webapi/server.go
Normal file
123
webapi/server.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package webapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/decred/dcrd/chaincfg/v3"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/jholdstock/dcrvsp/database"
|
||||||
|
"github.com/jrick/wsrpc/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
SignKey ed25519.PrivateKey
|
||||||
|
PubKey ed25519.PublicKey
|
||||||
|
VSPFee float64
|
||||||
|
NetParams *chaincfg.Params
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
var db *database.VspDatabase
|
||||||
|
var nodeConnection *wsrpc.Client
|
||||||
|
|
||||||
|
func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *sync.WaitGroup,
|
||||||
|
listen string, db *database.VspDatabase, nodeConnection *wsrpc.Client, releaseMode bool, config Config) error {
|
||||||
|
|
||||||
|
// Create TCP listener.
|
||||||
|
var listenConfig net.ListenConfig
|
||||||
|
listener, err := listenConfig.Listen(ctx, "tcp", listen)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("Listening on %s", listen)
|
||||||
|
|
||||||
|
srv := http.Server{
|
||||||
|
Handler: router(releaseMode),
|
||||||
|
ReadTimeout: 5 * time.Second, // slow requests should not hold connections opened
|
||||||
|
WriteTimeout: 60 * time.Second, // hung responses must die
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the graceful shutdown to the waitgroup.
|
||||||
|
shutdownWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
// Wait until shutdown is signaled before shutting down.
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
log.Debug("Stopping webserver...")
|
||||||
|
// Give the webserver 5 seconds to finish what it is doing.
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(timeoutCtx); err != nil {
|
||||||
|
log.Errorf("Failed to stop webserver cleanly: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Debug("Webserver stopped")
|
||||||
|
}
|
||||||
|
shutdownWg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start webserver.
|
||||||
|
go func() {
|
||||||
|
err = srv.Serve(listener)
|
||||||
|
// If the server dies for any reason other than ErrServerClosed (from
|
||||||
|
// graceful server.Shutdown), log the error and request dcrvsp be
|
||||||
|
// shutdown.
|
||||||
|
if err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Errorf("Unexpected webserver error: %v", err)
|
||||||
|
requestShutdownChan <- struct{}{}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cfg = config
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func router(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.GET("/fee", fee)
|
||||||
|
api.POST("/feeaddress", feeAddress)
|
||||||
|
api.GET("/pubkey", pubKey)
|
||||||
|
api.POST("/payfee", payFee)
|
||||||
|
api.POST("/setvotebits", setVoteBits)
|
||||||
|
api.POST("/ticketstatus", ticketStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func homepage(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "homepage.html", gin.H{
|
||||||
|
"Message": "Welcome to dcrvsp!",
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user