diff --git a/background/background.go b/background/background.go index 09c2add..a1f8344 100644 --- a/background/background.go +++ b/background/background.go @@ -18,9 +18,9 @@ type NotificationHandler struct { dcrdClient *rpc.DcrdRPC } -// The number of confirmations required to consider a ticket purchase or a fee -// transaction to be final. const ( + // requiredConfs is the number of confirmations required to consider a + // ticket purchase or a fee transaction to be final. requiredConfs = 6 ) diff --git a/config.go b/config.go index 1598ff2..fbd480a 100644 --- a/config.go +++ b/config.go @@ -19,7 +19,7 @@ import ( var ( defaultListen = ":3000" defaultLogLevel = "debug" - defaultVSPFee = 0.001 + defaultVSPFee = 0.05 defaultNetwork = "testnet" defaultHomeDir = dcrutil.AppDataDir("dcrvsp", false) defaultConfigFilename = "dcrvsp.conf" @@ -35,7 +35,7 @@ type config struct { LogLevel string `long:"loglevel" ini-name:"loglevel" description:"Logging level." choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"critical"` Network string `long:"network" ini-name:"network" description:"Decred network to use." choice:"testnet" choice:"mainnet" choice:"simnet"` FeeXPub string `long:"feexpub" ini-name:"feexpub" description:"Cold wallet xpub used for collecting fees."` - VSPFee float64 `long:"vspfee" ini-name:"vspfee" description:"Fee charged for VSP use. Absolute value - eg. 0.01 = 0.01 DCR."` + VSPFee float64 `long:"vspfee" ini-name:"vspfee" description:"Fee percentage charged for VSP use. eg. 0.01 (1%), 0.05 (5%)."` HomeDir string `long:"homedir" ini-name:"homedir" no-ini:"true" description:"Path to application home directory. Used for storing VSP database and logs."` ConfigFile string `long:"configfile" ini-name:"configfile" no-ini:"true" description:"Path to configuration file."` DcrdHost string `long:"dcrdhost" ini-name:"dcrdhost" description:"The ip:port to establish a JSON-RPC connection with dcrd. Should be the same host where dcrvsp is running."` diff --git a/database/database_test.go b/database/database_test.go index 907e097..b6b9ae7 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -19,7 +19,7 @@ func exampleTicket() Ticket { CommitmentAddress: "Address", FeeAddressIndex: 12345, FeeAddress: "FeeAddress", - VSPFee: 0.1, + FeeAmount: 0.1, FeeExpiration: 4, Confirmed: false, VoteChoices: map[string]string{"AgendaID": "Choice"}, @@ -106,7 +106,7 @@ func testGetTicketByHash(t *testing.T) { retrieved.CommitmentAddress != ticket.CommitmentAddress || retrieved.FeeAddressIndex != ticket.FeeAddressIndex || retrieved.FeeAddress != ticket.FeeAddress || - retrieved.VSPFee != ticket.VSPFee || + retrieved.FeeAmount != ticket.FeeAmount || retrieved.FeeExpiration != ticket.FeeExpiration || retrieved.Confirmed != ticket.Confirmed || !reflect.DeepEqual(retrieved.VoteChoices, ticket.VoteChoices) || diff --git a/database/ticket.go b/database/ticket.go index b6bdb95..9cfa9f1 100644 --- a/database/ticket.go +++ b/database/ticket.go @@ -17,7 +17,7 @@ type Ticket struct { CommitmentAddress string `json:"commitmentaddress"` FeeAddressIndex uint32 `json:"feeaddressindex"` FeeAddress string `json:"feeaddress"` - VSPFee float64 `json:"vspfee"` + FeeAmount float64 `json:"feeamount"` FeeExpiration int64 `json:"feeexpiration"` // Confirmed will be set when the ticket has 6+ confirmations. diff --git a/main.go b/main.go index 2324f45..f235f65 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ import ( ) const ( - defaultFeeAddressExpiration = 24 * time.Hour + defaultFeeAddressExpiration = 1 * time.Hour ) func main() { diff --git a/rpc/dcrd.go b/rpc/dcrd.go index 1c4129c..0ce640e 100644 --- a/rpc/dcrd.go +++ b/rpc/dcrd.go @@ -101,3 +101,19 @@ func (c *DcrdRPC) GetTicketCommitmentAddress(ticketHash string, netParams *chain func (c *DcrdRPC) NotifyBlocks() error { return c.Call(c.ctx, "notifyblocks", nil) } + +func (c *DcrdRPC) GetBestBlockHeader() (*dcrdtypes.GetBlockHeaderVerboseResult, error) { + var bestBlockHash string + err := c.Call(c.ctx, "getbestblockhash", &bestBlockHash) + if err != nil { + return nil, err + } + + verbose := true + var blockHeader dcrdtypes.GetBlockHeaderVerboseResult + err = c.Call(c.ctx, "getblockheader", &blockHeader, bestBlockHash, verbose) + if err != nil { + return nil, err + } + return &blockHeader, nil +} diff --git a/webapi/getfeeaddress.go b/webapi/getfeeaddress.go index d453129..6edc04e 100644 --- a/webapi/getfeeaddress.go +++ b/webapi/getfeeaddress.go @@ -5,6 +5,8 @@ import ( "sync" "time" + "decred.org/dcrwallet/wallet/txrules" + "github.com/decred/dcrd/dcrutil/v3" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/jholdstock/dcrvsp/database" @@ -35,6 +37,28 @@ func getNewFeeAddress(db *database.VspDatabase, addrGen *addressGenerator) (stri return addr, idx, nil } +func getCurrentFee(dcrdClient *rpc.DcrdRPC) (float64, error) { + bestBlock, err := dcrdClient.GetBestBlockHeader() + if err != nil { + return 0, err + } + sDiff, err := dcrutil.NewAmount(bestBlock.SBits) + if err != nil { + return 0, err + } + relayFee, err := dcrutil.NewAmount(relayFee) + if err != nil { + return 0, err + } + + fee := txrules.StakePoolTicketFee(sDiff, relayFee, int32(bestBlock.Height), + cfg.VSPFee, cfg.NetParams) + if err != nil { + return 0, err + } + return fee.ToCoin(), nil +} + // feeAddress is the handler for "POST /feeaddress". func feeAddress(c *gin.Context) { @@ -57,21 +81,29 @@ func feeAddress(c *gin.Context) { // If the expiry period has passed we need to issue a new fee. now := time.Now() if ticket.FeeExpired() { + newFee, err := getCurrentFee(dcrdClient) + if err != nil { + log.Errorf("getCurrentFee error: %v", err) + sendErrorResponse("fee error", http.StatusInternalServerError, c) + return + } ticket.FeeExpiration = now.Add(cfg.FeeAddressExpiration).Unix() - ticket.VSPFee = cfg.VSPFee + ticket.FeeAmount = newFee - err := db.UpdateTicket(ticket) + err = db.UpdateTicket(ticket) if err != nil { log.Errorf("UpdateTicket error: %v", err) sendErrorResponse("database error", http.StatusInternalServerError, c) return } + log.Debugf("Expired fee updated for ticket: newFeeAmt=%f, ticketHash=%s", + newFee, ticket.Hash) } sendJSONResponse(feeAddressResponse{ Timestamp: now.Unix(), Request: feeAddressRequest, FeeAddress: ticket.FeeAddress, - Fee: ticket.VSPFee, + FeeAmount: ticket.FeeAmount, Expiration: ticket.FeeExpiration, }, c) @@ -104,6 +136,13 @@ func feeAddress(c *gin.Context) { confirmed = true } + fee, err := getCurrentFee(dcrdClient) + if err != nil { + log.Errorf("getCurrentFee error: %v", err) + sendErrorResponse("fee error", http.StatusInternalServerError, c) + return + } + newAddress, newAddressIdx, err := getNewFeeAddress(db, addrGen) if err != nil { log.Errorf("getNewFeeAddress error: %v", err) @@ -118,7 +157,7 @@ func feeAddress(c *gin.Context) { FeeAddressIndex: newAddressIdx, FeeAddress: newAddress, Confirmed: confirmed, - VSPFee: cfg.VSPFee, + FeeAmount: fee, FeeExpiration: expire, // VotingKey and VoteChoices: set during payfee } @@ -131,13 +170,13 @@ func feeAddress(c *gin.Context) { } log.Debugf("Fee address created for new ticket: tktConfirmed=%t, feeAddrIdx=%d, "+ - "feeAddr=%s, ticketHash=%s", confirmed, newAddressIdx, newAddress, ticketHash) + "feeAddr=%s, feeAmt=%f, ticketHash=%s", confirmed, newAddressIdx, newAddress, fee, ticketHash) sendJSONResponse(feeAddressResponse{ Timestamp: now.Unix(), Request: feeAddressRequest, FeeAddress: newAddress, - Fee: cfg.VSPFee, + FeeAmount: fee, Expiration: expire, }, c) } diff --git a/webapi/payfee.go b/webapi/payfee.go index b09ea7c..5fc3347 100644 --- a/webapi/payfee.go +++ b/webapi/payfee.go @@ -89,7 +89,7 @@ func payFee(c *gin.Context) { // Loop through transaction outputs until we find one which pays to the // expected fee address. Record how much is being paid to the fee address. - var feeAmount dcrutil.Amount + var feePaid dcrutil.Amount const scriptVersion = 0 findAddress: @@ -103,13 +103,13 @@ findAddress: } for _, addr := range addresses { if addr.Address() == ticket.FeeAddress { - feeAmount = dcrutil.Amount(txOut.Value) + feePaid = dcrutil.Amount(txOut.Value) break findAddress } } } - if feeAmount == 0 { + if feePaid == 0 { log.Warnf("FeeTx for ticket %s did not include any payments for address %s", ticket.Hash, ticket.FeeAddress) sendErrorResponse("feetx did not include any payments for fee address", http.StatusBadRequest, c) return @@ -125,15 +125,15 @@ findAddress: // TODO: DB - validate votingkey against ticket submission address - minFee, err := dcrutil.NewAmount(cfg.VSPFee) + minFee, err := dcrutil.NewAmount(ticket.FeeAmount) if err != nil { log.Errorf("dcrutil.NewAmount: %v", err) sendErrorResponse("fee error", http.StatusInternalServerError, c) return } - if feeAmount < minFee { - log.Warnf("Fee too small: was %v, expected %v", feeAmount, minFee) + if feePaid < minFee { + log.Warnf("Fee too small from %s: was %v, expected %v", c.ClientIP(), feePaid, minFee) sendErrorResponse("fee too small", http.StatusInternalServerError, c) return } @@ -153,7 +153,8 @@ findAddress: return } - log.Debugf("Fee tx received for ticket: ticketHash=%s", ticket.Hash) + log.Debugf("Fee tx received for ticket: minExpectedFee=%v, feePaid=%v, "+ + "ticketHash=%s", minFee, feePaid, ticket.Hash) if ticket.Confirmed { feeTxHash, err := dcrdClient.SendRawTransaction(payFeeRequest.FeeTx) diff --git a/webapi/status.go b/webapi/status.go index 84071fe..75a025a 100644 --- a/webapi/status.go +++ b/webapi/status.go @@ -17,7 +17,7 @@ func pubKey(c *gin.Context) { // fee is the handler for "GET /fee". func fee(c *gin.Context) { sendJSONResponse(feeResponse{ - Timestamp: time.Now().Unix(), - Fee: cfg.VSPFee, + Timestamp: time.Now().Unix(), + FeePercentage: cfg.VSPFee, }, c) } diff --git a/webapi/templates/homepage.html b/webapi/templates/homepage.html index 2479d91..47caa4c 100644 --- a/webapi/templates/homepage.html +++ b/webapi/templates/homepage.html @@ -14,7 +14,7 @@ - +
Total tickets:{{ .TotalTickets }}
FeePaid tickets:{{ .FeePaidTickets }}
VSP Fee:{{ .VSPFee }} DCR per ticket
VSP Fee:{{ .VSPFee }}
Network:{{ .Network }}

Last updated: {{.UpdateTime}}

diff --git a/webapi/types.go b/webapi/types.go index 72c837a..3e723cf 100644 --- a/webapi/types.go +++ b/webapi/types.go @@ -6,8 +6,8 @@ type pubKeyResponse struct { } type feeResponse struct { - Timestamp int64 `json:"timestamp" binding:"required"` - Fee float64 `json:"fee" binding:"required"` + Timestamp int64 `json:"timestamp" binding:"required"` + FeePercentage float64 `json:"feepercentage" binding:"required"` } type FeeAddressRequest struct { @@ -18,7 +18,7 @@ type FeeAddressRequest struct { type feeAddressResponse struct { Timestamp int64 `json:"timestamp" binding:"required"` FeeAddress string `json:"feeaddress" binding:"required"` - Fee float64 `json:"fee" binding:"required"` + FeeAmount float64 `json:"feeamount" binding:"required"` Expiration int64 `json:"expiration" binding:"required"` Request FeeAddressRequest `json:"request" binding:"required"` } diff --git a/webapi/webapi.go b/webapi/webapi.go index 40e2fa4..fc18eb6 100644 --- a/webapi/webapi.go +++ b/webapi/webapi.go @@ -27,10 +27,12 @@ type Config struct { FeeAddressExpiration time.Duration } -// The number of confirmations required to consider a ticket purchase or a fee -// transaction to be final. 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