vspd/webapi/webapi.go
Jamie Holdstock ccafd8dec4
Calculate fee from percentage. (#69)
* Calculate fee from percentage.

- Reverted config to accept a fee percentage, not absolute value.
- The fee amount to be paid is now included in the `getfeeaddress` response. The current best block is used to calculate the fee percentage, and new blocks may be mined before the fee is paid, so the fee expiry period is shortened from 24 hours to 1 hour to mitigate this.
- Rename ticket db field to FeeAmount so it is more representative of the data it holds.
- API fields renamed to "FeePercentage" and "FeeAmount"
- Relay fee is still hard coded.

* Use getbestblockhash
2020-05-27 14:44:40 +01:00

227 lines
6.0 KiB
Go

package webapi
import (
"context"
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"net/http"
"sync"
"time"
"github.com/jholdstock/dcrvsp/database"
"github.com/jholdstock/dcrvsp/rpc"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/gin-gonic/gin"
)
type Config struct {
SignKey ed25519.PrivateKey
PubKey ed25519.PublicKey
VSPFee float64
NetParams *chaincfg.Params
FeeAccountName string
FeeAddressExpiration time.Duration
}
const (
// requiredConfs is the number of confirmations required to consider a
// ticket purchase or a fee transaction to be final.
requiredConfs = 6
// TODO: Make this configurable or get it from RPC.
relayFee = 0.0001
)
var homepageData *gin.H
var cfg Config
var db *database.VspDatabase
var dcrdConnect rpc.Connect
var walletConnect rpc.Connect
var addrGen *addressGenerator
func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *sync.WaitGroup,
listen string, vdb *database.VspDatabase, dConnect rpc.Connect, wConnect rpc.Connect, debugMode bool, feeXPub string, config Config) error {
// Populate template data before starting webserver.
var err error
homepageData, err = updateHomepageData(vdb, config)
if err != nil {
return fmt.Errorf("could not initialize homepage data: %v", err)
}
// Get the last used address index from the database, and use it to
// initialize the address generator.
idx, err := vdb.GetLastAddressIndex()
if err != nil {
return fmt.Errorf("GetLastAddressIndex error: %v", err)
}
addrGen, err = newAddressGenerator(feeXPub, config.NetParams, idx)
if err != nil {
return fmt.Errorf("failed to initialize fee address generator: %v", err)
}
// 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(debugMode),
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{}{}
}
}()
// Use a ticker to update template data.
var refresh time.Duration
if debugMode {
refresh = 1 * time.Second
} else {
refresh = 5 * time.Minute
}
shutdownWg.Add(1)
go func() {
ticker := time.NewTicker(refresh)
for {
select {
case <-ctx.Done():
ticker.Stop()
shutdownWg.Done()
return
case <-ticker.C:
homepageData, err = updateHomepageData(db, cfg)
if err != nil {
log.Errorf("Failed to update homepage data: %v", err)
}
}
}
}()
cfg = config
db = vdb
dcrdConnect = dConnect
walletConnect = wConnect
return nil
}
func router(debugMode 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 !debugMode {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
router.LoadHTMLGlob("webapi/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 debugMode {
// 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", "webapi/public/")
// These routes have no extra middleware. They can be accessed by anybody.
router.GET("/", homepage)
router.GET("/api/fee", fee)
router.GET("/api/pubkey", pubKey)
// These API routes access dcrd and they need authentication.
feeOnly := router.Group("/api").Use(
withDcrdClient(), vspAuth(),
)
feeOnly.POST("/feeaddress", feeAddress)
feeOnly.GET("/ticketstatus", ticketStatus)
feeOnly.POST("/payfee", payFee)
// These API routes access dcrd and the voting wallets, and they need
// authentication.
both := router.Group("/api").Use(
withDcrdClient(), withWalletClient(), vspAuth(),
)
both.POST("/setvotechoices", setVoteChoices)
return router
}
func updateHomepageData(db *database.VspDatabase, cfg Config) (*gin.H, error) {
total, feePaid, err := db.CountTickets()
if err != nil {
return nil, err
}
return &gin.H{
"Message": "Welcome to dcrvsp!",
"TotalTickets": total,
"FeePaidTickets": feePaid,
"VSPFee": cfg.VSPFee,
"Network": cfg.NetParams.Name,
"UpdateTime": time.Now().Format("Mon Jan _2 15:04:05 2006"),
}, nil
}
func homepage(c *gin.Context) {
c.HTML(http.StatusOK, "homepage.html", homepageData)
}
func sendJSONResponse(resp interface{}, c *gin.Context) {
dec, err := json.Marshal(resp)
if err != nil {
log.Errorf("JSON marshal error: %v", err)
sendErrorResponse("failed to marshal json", http.StatusInternalServerError, c)
return
}
sig := ed25519.Sign(cfg.SignKey, dec)
c.Writer.Header().Set("VSP-Server-Signature", hex.EncodeToString(sig))
c.AbortWithStatusJSON(http.StatusOK, resp)
}
func sendErrorResponse(errMsg string, code int, c *gin.Context) {
c.AbortWithStatusJSON(code, gin.H{"error": errMsg})
}