From ccafd8dec4fc23b2057f03c8f6e686dffeeb0a26 Mon Sep 17 00:00:00 2001 From: Jamie Holdstock Date: Wed, 27 May 2020 14:44:40 +0100 Subject: [PATCH] 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 --- background/background.go | 4 +-- config.go | 4 +-- database/database_test.go | 4 +-- database/ticket.go | 2 +- main.go | 2 +- rpc/dcrd.go | 16 +++++++++++ webapi/getfeeaddress.go | 51 ++++++++++++++++++++++++++++++---- webapi/payfee.go | 15 +++++----- webapi/status.go | 4 +-- webapi/templates/homepage.html | 2 +- webapi/types.go | 6 ++-- webapi/webapi.go | 6 ++-- 12 files changed, 87 insertions(+), 29 deletions(-) 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