Set vote choices on voting wallets (#43)

This commit is contained in:
Jamie Holdstock 2020-05-21 07:59:51 +01:00 committed by GitHub
parent bd518d7e24
commit bb416e8bc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 241 additions and 191 deletions

View File

@ -28,7 +28,7 @@
- Request fee address (`POST /feeaddress`)
- Pay fee (`POST /payFee`)
- Ticket status (`GET /ticketstatus`)
- Set voting preferences (`POST /setvotebits`)
- Set voting preferences (`POST /setvotechoices`)
- A minimal, static, web front-end providing pool stats and basic connection instructions.
- Fees have an expiry period. If the fee is not paid within this period, the
client must request a new fee. This enables the VSP to alter its fee rate.

View File

@ -3,6 +3,7 @@ package database
import (
"context"
"os"
"reflect"
"sync"
"testing"
)
@ -20,7 +21,7 @@ func exampleTicket() Ticket {
FeeAddress: "FeeAddress",
SDiff: 1,
BlockHeight: 2,
VoteBits: 3,
VoteChoices: map[string]string{"AgendaID": "Choice"},
VotingKey: "VotingKey",
VSPFee: 0.1,
Expiration: 4,
@ -38,7 +39,7 @@ func TestDatabase(t *testing.T) {
"testGetTicketByHash": testGetTicketByHash,
"testInsertFeeAddressVotingKey": testInsertFeeAddressVotingKey,
"testUpdateExpireAndFee": testUpdateExpireAndFee,
"testUpdateVoteBits": testUpdateVoteBits,
"testUpdateVoteChoices": testUpdateVoteChoices,
}
for testName, test := range tests {
@ -105,7 +106,7 @@ func testGetTicketByHash(t *testing.T) {
retrieved.FeeAddress != ticket.FeeAddress ||
retrieved.SDiff != ticket.SDiff ||
retrieved.BlockHeight != ticket.BlockHeight ||
retrieved.VoteBits != ticket.VoteBits ||
!reflect.DeepEqual(retrieved.VoteChoices, ticket.VoteChoices) ||
retrieved.VotingKey != ticket.VotingKey ||
retrieved.VSPFee != ticket.VSPFee ||
retrieved.Expiration != ticket.Expiration {
@ -129,10 +130,11 @@ func testInsertFeeAddressVotingKey(t *testing.T) {
// Update values.
newVotingKey := ticket.VotingKey + "2"
newVoteBits := ticket.VoteBits + 2
err = db.InsertFeeAddressVotingKey(ticket.CommitmentAddress, newVotingKey, newVoteBits)
newVoteChoices := ticket.VoteChoices
newVoteChoices["AgendaID"] = "Different choice"
err = db.InsertFeeAddressVotingKey(ticket.CommitmentAddress, newVotingKey, newVoteChoices)
if err != nil {
t.Fatalf("error updating votingkey and votebits: %v", err)
t.Fatalf("error updating votingkey and votechoices: %v", err)
}
// Retrieve ticket from database.
@ -142,7 +144,7 @@ func testInsertFeeAddressVotingKey(t *testing.T) {
}
// Check ticket fields match expected.
if newVoteBits != retrieved.VoteBits ||
if !reflect.DeepEqual(newVoteChoices, retrieved.VoteChoices) ||
newVotingKey != retrieved.VotingKey {
t.Fatal("retrieved ticket value didnt match expected")
}
@ -176,7 +178,7 @@ func testUpdateExpireAndFee(t *testing.T) {
}
}
func testUpdateVoteBits(t *testing.T) {
func testUpdateVoteChoices(t *testing.T) {
// Insert a ticket into the database.
ticket := exampleTicket()
err := db.InsertFeeAddress(ticket)
@ -184,11 +186,12 @@ func testUpdateVoteBits(t *testing.T) {
t.Fatalf("error storing ticket in database: %v", err)
}
// Update ticket with new votebits.
newVoteBits := ticket.VoteBits + 1
err = db.UpdateVoteBits(ticket.Hash, newVoteBits)
// Update ticket with new votechoices.
newVoteChoices := ticket.VoteChoices
newVoteChoices["AgendaID"] = "Different choice"
err = db.UpdateVoteChoices(ticket.Hash, newVoteChoices)
if err != nil {
t.Fatalf("error updating votebits: %v", err)
t.Fatalf("error updating votechoices: %v", err)
}
// Get updated ticket
@ -198,7 +201,7 @@ func testUpdateVoteBits(t *testing.T) {
}
// Check ticket fields match expected.
if retrieved.VoteBits != newVoteBits {
if !reflect.DeepEqual(newVoteChoices, retrieved.VoteChoices) {
t.Fatal("retrieved ticket value didnt match expected")
}
}

View File

@ -9,16 +9,16 @@ import (
)
type Ticket struct {
Hash string `json:"hash"`
CommitmentSignature string `json:"commitmentsignature"`
CommitmentAddress string `json:"commitmentaddress"`
FeeAddress string `json:"feeaddress"`
SDiff float64 `json:"sdiff"`
BlockHeight int64 `json:"blockheight"`
VoteBits uint16 `json:"votebits"`
VotingKey string `json:"votingkey"`
VSPFee float64 `json:"vspfee"`
Expiration int64 `json:"expiration"`
Hash string `json:"hash"`
CommitmentSignature string `json:"commitmentsignature"`
CommitmentAddress string `json:"commitmentaddress"`
FeeAddress string `json:"feeaddress"`
SDiff float64 `json:"sdiff"`
BlockHeight int64 `json:"blockheight"`
VoteChoices map[string]string `json:"votechoices"`
VotingKey string `json:"votingkey"`
VSPFee float64 `json:"vspfee"`
Expiration int64 `json:"expiration"`
}
var (
@ -43,7 +43,7 @@ func (vdb *VspDatabase) InsertFeeAddress(ticket Ticket) error {
})
}
func (vdb *VspDatabase) InsertFeeAddressVotingKey(address, votingKey string, voteBits uint16) error {
func (vdb *VspDatabase) InsertFeeAddressVotingKey(address, votingKey string, voteChoices map[string]string) error {
return vdb.db.Update(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
c := ticketBkt.Cursor()
@ -57,7 +57,7 @@ func (vdb *VspDatabase) InsertFeeAddressVotingKey(address, votingKey string, vot
if ticket.CommitmentAddress == address {
ticket.VotingKey = votingKey
ticket.VoteBits = voteBits
ticket.VoteChoices = voteChoices
ticketBytes, err := json.Marshal(ticket)
if err != nil {
return err
@ -94,7 +94,7 @@ func (vdb *VspDatabase) GetTicketByHash(hash string) (Ticket, error) {
return ticket, err
}
func (vdb *VspDatabase) UpdateVoteBits(hash string, voteBits uint16) error {
func (vdb *VspDatabase) UpdateVoteChoices(hash string, voteChoices map[string]string) error {
return vdb.db.Update(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
key := []byte(hash)
@ -109,7 +109,7 @@ func (vdb *VspDatabase) UpdateVoteBits(hash string, voteBits uint16) error {
if err != nil {
return fmt.Errorf("could not unmarshal ticket: %v", err)
}
ticket.VoteBits = voteBits
ticket.VoteChoices = voteChoices
ticketBytes, err = json.Marshal(ticket)
if err != nil {

View File

@ -38,7 +38,7 @@ func feeAddress(c *gin.Context) {
// signature - sanity check signature is in base64 encoding
signature := feeAddressRequest.Signature
if _, err = base64.StdEncoding.DecodeString(signature); err != nil {
log.Warnf("Invalid signature from %s", c.ClientIP())
log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err)
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
return
}
@ -145,7 +145,7 @@ func feeAddress(c *gin.Context) {
message := fmt.Sprintf("vsp v3 getfeeaddress %s", msgTx.TxHash())
err = dcrutil.VerifyMessage(addr.Address(), signature, message, cfg.NetParams)
if err != nil {
log.Warnf("Invalid signature from %s", c.ClientIP())
log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err)
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
return
}
@ -179,10 +179,9 @@ func feeAddress(c *gin.Context) {
FeeAddress: newAddress,
SDiff: blockHeader.SBits,
BlockHeight: int64(blockHeader.Height),
VoteBits: dcrutil.BlockValid,
VSPFee: cfg.VSPFee,
Expiration: expire,
// VotingKey: set during payfee
// VotingKey and VoteChoices: set during payfee
}
err = db.InsertFeeAddress(dbTicket)

View File

@ -1,41 +1,43 @@
package webapi
import (
"fmt"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrutil/v3"
)
// isValidVoteBits checks if voteBits are valid for the most recent agendas.
func isValidVoteBits(params *chaincfg.Params, voteBits uint16) bool {
if !dcrutil.IsFlagSet16(voteBits, dcrutil.BlockValid) {
return false
}
voteBits &= ^uint16(dcrutil.BlockValid)
// Get the most recent vote version.
var voteVersion uint32
func currentVoteVersion(params *chaincfg.Params) uint32 {
var latestVersion uint32
for version := range params.Deployments {
if voteVersion < version {
voteVersion = version
if latestVersion < version {
latestVersion = version
}
}
var availVoteBits uint16
for _, vote := range params.Deployments[voteVersion] {
availVoteBits |= vote.Vote.Mask
isValid := false
maskedBits := voteBits & vote.Vote.Mask
for _, c := range vote.Vote.Choices {
if c.Bits == maskedBits {
isValid = true
break
}
}
if !isValid {
return false
}
}
return true
return latestVersion
}
// isValidVoteChoices returns an error if provided vote choices are not valid for
// the most recent agendas.
func isValidVoteChoices(params *chaincfg.Params, voteVersion uint32, voteChoices map[string]string) error {
agendaLoop:
for agenda, choice := range voteChoices {
// Does the agenda exist?
for _, v := range params.Deployments[voteVersion] {
if v.Vote.Id == agenda {
// Agenda exists - does the vote choice exist?
for _, c := range v.Vote.Choices {
if c.Id == choice {
// Valid agenda and choice combo! Check the next one...
continue agendaLoop
}
}
return fmt.Errorf("choice %q not found for agenda %q", choice, agenda)
}
}
return fmt.Errorf("agenda %q not found for vote version %d", agenda, voteVersion)
}
return nil
}

View File

@ -4,31 +4,47 @@ import (
"testing"
"github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/dcrutil/v3"
)
func TestVoteBits(t *testing.T) {
func TestIsValidVoteChoices(t *testing.T) {
// Mainnet vote version 4 contains 2 agendas - sdiffalgorithm and lnsupport.
// Both agendas have vote choices yes/no/abstain.
voteVersion := uint32(4)
params := chaincfg.MainNetParams()
var tests = []struct {
voteBits uint16
isValid bool
voteChoices map[string]string
valid bool
}{
{0, false},
{dcrutil.BlockValid, true},
{dcrutil.BlockValid | 0x0002, true},
{dcrutil.BlockValid | 0x0003, true},
{dcrutil.BlockValid | 0x0004, true},
{dcrutil.BlockValid | 0x0005, true},
{dcrutil.BlockValid | 0x0006, false},
{dcrutil.BlockValid | 0x0007, false},
{dcrutil.BlockValid | 0x0008, true},
// Empty vote choices are allowed.
{map[string]string{}, true},
// Valid agenda, valid vote choice.
{map[string]string{"lnsupport": "yes"}, true},
{map[string]string{"sdiffalgorithm": "no", "lnsupport": "yes"}, true},
// Invalid agenda.
{map[string]string{"": "yes"}, false},
{map[string]string{"Fake agenda": "yes"}, false},
// Valid agenda, invalid vote choice.
{map[string]string{"lnsupport": "1234"}, false},
{map[string]string{"sdiffalgorithm": ""}, false},
// One valid choice, one invalid choice.
{map[string]string{"sdiffalgorithm": "no", "lnsupport": "1234"}, false},
{map[string]string{"sdiffalgorithm": "1234", "lnsupport": "no"}, false},
// One valid agenda, one invalid agenda.
{map[string]string{"fake": "abstain", "lnsupport": "no"}, false},
{map[string]string{"sdiffalgorithm": "abstain", "": "no"}, false},
}
params := chaincfg.MainNetParams()
for _, test := range tests {
isValid := isValidVoteBits(params, test.voteBits)
if isValid != test.isValid {
t.Fatalf("isValidVoteBits failed for votebits '%d': want %v, got %v",
test.voteBits, test.isValid, isValid)
err := isValidVoteChoices(params, voteVersion, test.voteChoices)
if (err == nil) != test.valid {
t.Fatalf("isValidVoteChoices failed for votechoices '%v'.", test.voteChoices)
}
}
}

View File

@ -33,10 +33,11 @@ func payFee(c *gin.Context) {
return
}
voteBits := payFeeRequest.VoteBits
if !isValidVoteBits(cfg.NetParams, voteBits) {
log.Warnf("Invalid votebits from %s", c.ClientIP())
sendErrorResponse("invalid votebits", http.StatusBadRequest, c)
voteChoices := payFeeRequest.VoteChoices
err = isValidVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteChoices)
if err != nil {
log.Warnf("Invalid votechoices from %s: %v", c.ClientIP(), err)
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
return
}
@ -161,6 +162,16 @@ findAddress:
return
}
// Update vote choices on voting wallets.
for agenda, choice := range voteChoices {
err = walletClient.Call(ctx, "setvotechoice", nil, agenda, choice, ticket.Hash)
if err != nil {
log.Errorf("setvotechoice failed: %v", err)
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
return
}
}
feeTxBuf := new(bytes.Buffer)
feeTxBuf.Grow(feeTx.SerializeSize())
err = feeTx.Serialize(feeTxBuf)
@ -178,7 +189,7 @@ findAddress:
return
}
err = db.InsertFeeAddressVotingKey(voteAddr.Address(), votingWIF.String(), voteBits)
err = db.InsertFeeAddressVotingKey(voteAddr.Address(), votingWIF.String(), voteChoices)
if err != nil {
log.Errorf("InsertFeeAddressVotingKey failed: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c)

View File

@ -1,80 +0,0 @@
package webapi
import (
"encoding/base64"
"fmt"
"net/http"
"time"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrutil/v3"
"github.com/gin-gonic/gin"
)
// setVoteBits is the handler for "POST /setvotebits"
func setVoteBits(c *gin.Context) {
var setVoteBitsRequest SetVoteBitsRequest
if err := c.ShouldBindJSON(&setVoteBitsRequest); err != nil {
log.Warnf("Bad setvotebits request from %s: %v", c.ClientIP(), err)
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
return
}
// ticketHash
ticketHashStr := setVoteBitsRequest.TicketHash
txHash, err := chainhash.NewHashFromStr(ticketHashStr)
if err != nil {
log.Warnf("Invalid ticket hash from %s", c.ClientIP())
sendErrorResponse("invalid ticket hash", http.StatusBadRequest, c)
return
}
// signature - sanity check signature is in base64 encoding
signature := setVoteBitsRequest.Signature
if _, err = base64.StdEncoding.DecodeString(signature); err != nil {
log.Warnf("Invalid signature from %s", c.ClientIP())
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
return
}
// votebits
voteBits := setVoteBitsRequest.VoteBits
if !isValidVoteBits(cfg.NetParams, voteBits) {
log.Warnf("Invalid votebits from %s", c.ClientIP())
sendErrorResponse("invalid votebits", http.StatusBadRequest, c)
return
}
ticket, err := db.GetTicketByHash(txHash.String())
if err != nil {
log.Warnf("Invalid ticket from %s", c.ClientIP())
sendErrorResponse("invalid ticket", http.StatusBadRequest, c)
return
}
// verify message
message := fmt.Sprintf("vsp v3 setvotebits %d %s %d", setVoteBitsRequest.Timestamp, txHash, voteBits)
err = dcrutil.VerifyMessage(ticket.CommitmentAddress, signature, message, cfg.NetParams)
if err != nil {
log.Warnf("Failed to verify message from %s", c.ClientIP())
sendErrorResponse("message did not pass verification", http.StatusBadRequest, c)
return
}
err = db.UpdateVoteBits(txHash.String(), voteBits)
if err != nil {
log.Errorf("UpdateVoteBits error: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c)
return
}
// TODO: DB - error if given timestamp is older than any previous requests
// TODO: DB - store setvotebits receipt in log
sendJSONResponse(setVoteBitsResponse{
Timestamp: time.Now().Unix(),
Request: setVoteBitsRequest,
VoteBits: voteBits,
}, c)
}

99
webapi/setvotechoices.go Normal file
View File

@ -0,0 +1,99 @@
package webapi
import (
"encoding/base64"
"fmt"
"net/http"
"time"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrutil/v3"
"github.com/gin-gonic/gin"
)
// setVoteChoices is the handler for "POST /setvotechoices"
func setVoteChoices(c *gin.Context) {
var setVoteChoicesRequest SetVoteChoicesRequest
if err := c.ShouldBindJSON(&setVoteChoicesRequest); err != nil {
log.Warnf("Bad setvotechoices request from %s: %v", c.ClientIP(), err)
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
return
}
// ticketHash
ticketHashStr := setVoteChoicesRequest.TicketHash
txHash, err := chainhash.NewHashFromStr(ticketHashStr)
if err != nil {
log.Warnf("Invalid ticket hash from %s", c.ClientIP())
sendErrorResponse("invalid ticket hash", http.StatusBadRequest, c)
return
}
// signature - sanity check signature is in base64 encoding
signature := setVoteChoicesRequest.Signature
if _, err = base64.StdEncoding.DecodeString(signature); err != nil {
log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err)
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
return
}
voteChoices := setVoteChoicesRequest.VoteChoices
err = isValidVoteChoices(cfg.NetParams, currentVoteVersion(cfg.NetParams), voteChoices)
if err != nil {
log.Warnf("Invalid votechoices from %s: %v", c.ClientIP(), err)
sendErrorResponse(err.Error(), http.StatusBadRequest, c)
return
}
ticket, err := db.GetTicketByHash(txHash.String())
if err != nil {
log.Warnf("Invalid ticket from %s", c.ClientIP())
sendErrorResponse("invalid ticket", http.StatusBadRequest, c)
return
}
// verify message
message := fmt.Sprintf("vsp v3 setvotechoices %d %s %v", setVoteChoicesRequest.Timestamp, txHash, voteChoices)
err = dcrutil.VerifyMessage(ticket.CommitmentAddress, signature, message, cfg.NetParams)
if err != nil {
log.Warnf("Failed to verify message from %s: %v", c.ClientIP(), err)
sendErrorResponse("message did not pass verification", http.StatusBadRequest, c)
return
}
walletClient, err := walletRPC()
if err != nil {
log.Errorf("Failed to dial dcrwallet RPC: %v", err)
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
return
}
ctx := c.Request.Context()
// Update vote choices on voting wallets.
for agenda, choice := range voteChoices {
err = walletClient.Call(ctx, "setvotechoice", nil, agenda, choice, ticket.Hash)
if err != nil {
log.Errorf("setvotechoice failed: %v", err)
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
return
}
}
err = db.UpdateVoteChoices(txHash.String(), voteChoices)
if err != nil {
log.Errorf("UpdateVoteChoices error: %v", err)
sendErrorResponse("database error", http.StatusInternalServerError, c)
return
}
// TODO: DB - error if given timestamp is older than any previous requests
// TODO: DB - store setvotechoices receipt in log
sendJSONResponse(setVoteChoicesResponse{
Timestamp: time.Now().Unix(),
Request: setVoteChoicesRequest,
VoteChoices: voteChoices,
}, c)
}

View File

@ -32,7 +32,7 @@ func ticketStatus(c *gin.Context) {
// signature - sanity check signature is in base64 encoding
signature := ticketStatusRequest.Signature
if _, err = base64.StdEncoding.DecodeString(signature); err != nil {
log.Warnf("Invalid signature from %s", c.ClientIP())
log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err)
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
return
}
@ -48,15 +48,15 @@ func ticketStatus(c *gin.Context) {
message := fmt.Sprintf("vsp v3 ticketstatus %d %s", ticketStatusRequest.Timestamp, ticketHashStr)
err = dcrutil.VerifyMessage(ticket.CommitmentAddress, signature, message, cfg.NetParams)
if err != nil {
log.Warnf("Invalid signature from %s", c.ClientIP())
log.Warnf("Invalid signature from %s: %v", c.ClientIP(), err)
sendErrorResponse("invalid signature", http.StatusBadRequest, c)
return
}
sendJSONResponse(ticketStatusResponse{
Timestamp: time.Now().Unix(),
Request: ticketStatusRequest,
Status: "active", // TODO - active, pending, expired (missed, revoked?)
VoteBits: ticket.VoteBits,
Timestamp: time.Now().Unix(),
Request: ticketStatusRequest,
Status: "active", // TODO - active, pending, expired (missed, revoked?)
VoteChoices: ticket.VoteChoices,
}, c)
}

View File

@ -25,11 +25,11 @@ type feeAddressResponse struct {
}
type PayFeeRequest struct {
Timestamp int64 `json:"timestamp" binding:"required"`
TicketHash string `json:"tickethash" binding:"required"`
FeeTx string `json:"feetx" binding:"required"`
VotingKey string `json:"votingkey" binding:"required"`
VoteBits uint16 `json:"votebits" binding:"required"`
Timestamp int64 `json:"timestamp" binding:"required"`
TicketHash string `json:"tickethash" binding:"required"`
FeeTx string `json:"feetx" binding:"required"`
VotingKey string `json:"votingkey" binding:"required"`
VoteChoices map[string]string `json:"votechoices" binding:"required"`
}
type payFeeResponse struct {
@ -38,17 +38,17 @@ type payFeeResponse struct {
Request PayFeeRequest `json:"request" binding:"required"`
}
type SetVoteBitsRequest struct {
Timestamp int64 `json:"timestamp" binding:"required"`
TicketHash string `json:"tickethash" binding:"required"`
Signature string `json:"commitmentsignature" binding:"required"`
VoteBits uint16 `json:"votebits" binding:"required"`
type SetVoteChoicesRequest struct {
Timestamp int64 `json:"timestamp" binding:"required"`
TicketHash string `json:"tickethash" binding:"required"`
Signature string `json:"commitmentsignature" binding:"required"`
VoteChoices map[string]string `json:"votechoices" binding:"required"`
}
type setVoteBitsResponse struct {
Timestamp int64 `json:"timestamp" binding:"required"`
Request SetVoteBitsRequest `json:"request" binding:"required"`
VoteBits uint16 `json:"votebits" binding:"required"`
type setVoteChoicesResponse struct {
Timestamp int64 `json:"timestamp" binding:"required"`
Request SetVoteChoicesRequest `json:"request" binding:"required"`
VoteChoices map[string]string `json:"votechoices" binding:"required"`
}
type TicketStatusRequest struct {
@ -58,8 +58,8 @@ type TicketStatusRequest struct {
}
type ticketStatusResponse struct {
Timestamp int64 `json:"timestamp" binding:"required"`
Request TicketStatusRequest `json:"request" binding:"required"`
Status string `json:"status" binding:"required"`
VoteBits uint16 `json:"votebits" binding:"required"`
Timestamp int64 `json:"timestamp" binding:"required"`
Request TicketStatusRequest `json:"request" binding:"required"`
Status string `json:"status" binding:"required"`
VoteChoices map[string]string `json:"votechoices" binding:"required"`
}

View File

@ -116,7 +116,7 @@ func router(debugMode bool) *gin.Engine {
api.POST("/feeaddress", feeAddress)
api.GET("/pubkey", pubKey)
api.POST("/payfee", payFee)
api.POST("/setvotebits", setVoteBits)
api.POST("/setvotechoices", setVoteChoices)
api.GET("/ticketstatus", ticketStatus)
}