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:
parent
87500c3fef
commit
ccafd8dec4
@ -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
|
||||
)
|
||||
|
||||
|
||||
@ -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."`
|
||||
|
||||
@ -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) ||
|
||||
|
||||
@ -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.
|
||||
|
||||
2
main.go
2
main.go
@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFeeAddressExpiration = 24 * time.Hour
|
||||
defaultFeeAddressExpiration = 1 * time.Hour
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
16
rpc/dcrd.go
16
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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user