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
This commit is contained in:
Jamie Holdstock 2020-05-27 14:44:40 +01:00 committed by GitHub
parent 87500c3fef
commit ccafd8dec4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 87 additions and 29 deletions

View File

@ -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
)

View File

@ -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."`

View File

@ -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) ||

View File

@ -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.

View File

@ -15,7 +15,7 @@ import (
)
const (
defaultFeeAddressExpiration = 24 * time.Hour
defaultFeeAddressExpiration = 1 * time.Hour
)
func main() {

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -18,6 +18,6 @@ func pubKey(c *gin.Context) {
func fee(c *gin.Context) {
sendJSONResponse(feeResponse{
Timestamp: time.Now().Unix(),
Fee: cfg.VSPFee,
FeePercentage: cfg.VSPFee,
}, c)
}

View File

@ -14,7 +14,7 @@
<table>
<tr><td>Total tickets:</td><td>{{ .TotalTickets }}</td></tr>
<tr><td>FeePaid tickets:</td><td>{{ .FeePaidTickets }}</td></tr>
<tr><td>VSP Fee:</td><td>{{ .VSPFee }} DCR per ticket</td></tr>
<tr><td>VSP Fee:</td><td>{{ .VSPFee }}</td></tr>
<tr><td>Network:</td><td>{{ .Network }}</td></tr>
</table>
<p>Last updated: {{.UpdateTime}}</p>

View File

@ -7,7 +7,7 @@ type pubKeyResponse struct {
type feeResponse struct {
Timestamp int64 `json:"timestamp" binding:"required"`
Fee float64 `json:"fee" 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"`
}

View File

@ -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