Delay fee broadcast and adding tickets to wallets. (#62)
* Delay fee broadcast and adding tickets to wallets. - Adds a `background` package which implements a dcrd notification handler. On each blockconnected notification, tickets with 6+ confirmations are marked confirmed, relevant fee transactions are broadcast, and any fees with 6+ confirmations have their tickets added to voting wallets. - VSP fee is now an absolute value measured in DCR rather than a percentage. This simplifies the code and is more appropriate for an MVP. We can re-add percentage based fees later. - Database code for tickets is now simplified to just "Insert/Update", rather than having functions for updating particular fields. - Pay fee response no longer includes the fee tx hash, because we dont necessarily broadcast the fee tx straight away. * Const for required confs
This commit is contained in:
parent
86c4195931
commit
87500c3fef
229
background/background.go
Normal file
229
background/background.go
Normal file
@ -0,0 +1,229 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"decred.org/dcrwallet/rpc/client/dcrd"
|
||||
"github.com/jholdstock/dcrvsp/database"
|
||||
"github.com/jholdstock/dcrvsp/rpc"
|
||||
)
|
||||
|
||||
type NotificationHandler struct {
|
||||
Ctx context.Context
|
||||
Db *database.VspDatabase
|
||||
WalletConnect rpc.Connect
|
||||
closed chan struct{}
|
||||
dcrdClient *rpc.DcrdRPC
|
||||
}
|
||||
|
||||
// The number of confirmations required to consider a ticket purchase or a fee
|
||||
// transaction to be final.
|
||||
const (
|
||||
requiredConfs = 6
|
||||
)
|
||||
|
||||
// Notify is called every time a block notification is received from dcrd.
|
||||
// Notify is never called concurrently. Notify should not return an error
|
||||
// because that will cause the client to close and no further notifications will
|
||||
// be received until a new connection is established.
|
||||
func (n *NotificationHandler) Notify(method string, params json.RawMessage) error {
|
||||
if method != "blockconnected" {
|
||||
return nil
|
||||
}
|
||||
|
||||
header, _, err := dcrd.BlockConnected(params)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to parse dcrd block notification: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debugf("Block notification %d (%s)", header.Height, header.BlockHash().String())
|
||||
|
||||
// Step 1/3: Update the database with any tickets which now have 6+
|
||||
// confirmations.
|
||||
|
||||
unconfirmed, err := n.Db.GetUnconfirmedTickets()
|
||||
if err != nil {
|
||||
log.Errorf("GetUnconfirmedTickets error: %v", err)
|
||||
}
|
||||
|
||||
for _, ticket := range unconfirmed {
|
||||
tktTx, err := n.dcrdClient.GetRawTransaction(ticket.Hash)
|
||||
if err != nil {
|
||||
log.Errorf("GetRawTransaction error: %v", err)
|
||||
continue
|
||||
}
|
||||
if tktTx.Confirmations >= requiredConfs {
|
||||
ticket.Confirmed = true
|
||||
err = n.Db.UpdateTicket(ticket)
|
||||
if err != nil {
|
||||
log.Errorf("UpdateTicket error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("Ticket confirmed: ticketHash=%s", ticket.Hash)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2/3: Broadcast fee tx for tickets which are confirmed.
|
||||
|
||||
pending, err := n.Db.GetPendingFees()
|
||||
if err != nil {
|
||||
log.Errorf("GetPendingFees error: %v", err)
|
||||
}
|
||||
|
||||
for _, ticket := range pending {
|
||||
feeTxHash, err := n.dcrdClient.SendRawTransaction(ticket.FeeTxHex)
|
||||
if err != nil {
|
||||
// TODO: SendRawTransaction can return a "transcation already
|
||||
// exists" error, which isnt necessarily a problem here.
|
||||
log.Errorf("SendRawTransaction error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
ticket.FeeTxHash = feeTxHash
|
||||
err = n.Db.UpdateTicket(ticket)
|
||||
if err != nil {
|
||||
log.Errorf("UpdateTicket error: %v", err)
|
||||
continue
|
||||
}
|
||||
log.Debugf("Fee tx broadcast for ticket: ticketHash=%s, feeHash=%s", ticket.Hash, feeTxHash)
|
||||
}
|
||||
|
||||
// Step 3/3: Add tickets with confirmed fees to voting wallets.
|
||||
|
||||
unconfirmedFees, err := n.Db.GetUnconfirmedFees()
|
||||
if err != nil {
|
||||
log.Errorf("GetUnconfirmedFees error: %v", err)
|
||||
// If this fails, there is nothing more we can do. Return.
|
||||
return nil
|
||||
}
|
||||
|
||||
// If there are no confirmed fees, there is nothing more to do. Return.
|
||||
if len(unconfirmedFees) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var walletClient *rpc.WalletRPC
|
||||
walletConn, err := n.WalletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("dcrwallet connection error: %v", err)
|
||||
// If this fails, there is nothing more we can do. Return.
|
||||
return nil
|
||||
}
|
||||
walletClient, err = rpc.WalletClient(n.Ctx, walletConn)
|
||||
if err != nil {
|
||||
log.Errorf("dcrwallet client error: %v", err)
|
||||
// If this fails, there is nothing more we can do. Return.
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ticket := range unconfirmedFees {
|
||||
feeTx, err := n.dcrdClient.GetRawTransaction(ticket.FeeTxHash)
|
||||
if err != nil {
|
||||
log.Errorf("GetRawTransaction error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// If fee is confirmed, update the database and add ticket to voting
|
||||
// wallets.
|
||||
if feeTx.Confirmations >= requiredConfs {
|
||||
ticket.FeeConfirmed = true
|
||||
err = n.Db.UpdateTicket(ticket)
|
||||
if err != nil {
|
||||
log.Errorf("UpdateTicket error: %v", err)
|
||||
return nil
|
||||
}
|
||||
log.Debugf("Fee tx confirmed for ticket: ticketHash=%s", ticket.Hash)
|
||||
|
||||
// Add ticket to the voting wallet.
|
||||
|
||||
rawTicket, err := n.dcrdClient.GetRawTransaction(ticket.Hash)
|
||||
if err != nil {
|
||||
log.Errorf("GetRawTransaction error: %v", err)
|
||||
continue
|
||||
}
|
||||
err = walletClient.AddTransaction(rawTicket.BlockHash, rawTicket.Hex)
|
||||
if err != nil {
|
||||
log.Errorf("AddTransaction error: %v", err)
|
||||
continue
|
||||
}
|
||||
err = walletClient.ImportPrivKey(ticket.VotingWIF)
|
||||
if err != nil {
|
||||
log.Errorf("ImportPrivKey error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Update vote choices on voting wallets.
|
||||
for agenda, choice := range ticket.VoteChoices {
|
||||
err = walletClient.SetVoteChoice(agenda, choice, ticket.Hash)
|
||||
if err != nil {
|
||||
log.Errorf("SetVoteChoice error: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
log.Debugf("Ticket added to voting wallet: ticketHash=%s", ticket.Hash)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NotificationHandler) Close() error {
|
||||
close(n.closed)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NotificationHandler) connect(dcrdConnect rpc.Connect) error {
|
||||
dcrdConn, err := dcrdConnect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.dcrdClient, err = rpc.DcrdClient(n.Ctx, dcrdConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = n.dcrdClient.NotifyBlocks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("Subscribed for dcrd block notifications")
|
||||
|
||||
// Wait until context is done (dcrvsp is shutting down), or until the
|
||||
// notifier is closed.
|
||||
select {
|
||||
case <-n.Ctx.Done():
|
||||
return n.Ctx.Err()
|
||||
case <-n.closed:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func Start(n *NotificationHandler, dcrdConnect rpc.Connect) {
|
||||
|
||||
// Loop forever attempting to create a connection to the dcrd server.
|
||||
go func() {
|
||||
for {
|
||||
n.closed = make(chan struct{})
|
||||
|
||||
err := n.connect(dcrdConnect)
|
||||
if err != nil {
|
||||
log.Errorf("dcrd connect error: %v", err)
|
||||
|
||||
// If context is done (dcrvsp is shutting down), return,
|
||||
// otherwise wait 15 seconds and to reconnect.
|
||||
select {
|
||||
case <-n.Ctx.Done():
|
||||
return
|
||||
case <-time.After(15 * time.Second):
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}()
|
||||
}
|
||||
26
background/log.go
Normal file
26
background/log.go
Normal file
@ -0,0 +1,26 @@
|
||||
package background
|
||||
|
||||
import (
|
||||
"github.com/decred/slog"
|
||||
)
|
||||
|
||||
// log is a logger that is initialized with no output filters. This
|
||||
// means the package will not perform any logging by default until the caller
|
||||
// requests it.
|
||||
var log slog.Logger
|
||||
|
||||
// The default amount of logging is none.
|
||||
func init() {
|
||||
DisableLog()
|
||||
}
|
||||
|
||||
// DisableLog disables all library log output. Logging output is disabled
|
||||
// by default until UseLogger is called.
|
||||
func DisableLog() {
|
||||
log = slog.Disabled
|
||||
}
|
||||
|
||||
// UseLogger uses a specified Logger to output package logging info.
|
||||
func UseLogger(logger slog.Logger) {
|
||||
log = logger
|
||||
}
|
||||
@ -19,7 +19,7 @@ import (
|
||||
var (
|
||||
defaultListen = ":3000"
|
||||
defaultLogLevel = "debug"
|
||||
defaultVSPFee = 0.01
|
||||
defaultVSPFee = 0.001
|
||||
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 percentage charged for VSP use. eg. 0.01 (1%), 0.05 (5%)."`
|
||||
VSPFee float64 `long:"vspfee" ini-name:"vspfee" description:"Fee charged for VSP use. Absolute value - eg. 0.01 = 0.01 DCR."`
|
||||
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,13 +19,14 @@ func exampleTicket() Ticket {
|
||||
CommitmentAddress: "Address",
|
||||
FeeAddressIndex: 12345,
|
||||
FeeAddress: "FeeAddress",
|
||||
SDiff: 1,
|
||||
BlockHeight: 2,
|
||||
VoteChoices: map[string]string{"AgendaID": "Choice"},
|
||||
VotingKey: "VotingKey",
|
||||
VSPFee: 0.1,
|
||||
FeeExpiration: 4,
|
||||
Confirmed: false,
|
||||
VoteChoices: map[string]string{"AgendaID": "Choice"},
|
||||
VotingWIF: "VotingKey",
|
||||
FeeTxHex: "FeeTransction",
|
||||
FeeTxHash: "",
|
||||
FeeConfirmed: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,11 +37,8 @@ func TestDatabase(t *testing.T) {
|
||||
|
||||
// All sub-tests to run.
|
||||
tests := map[string]func(*testing.T){
|
||||
"testInsertTicket": testInsertTicket,
|
||||
"testGetTicketByHash": testGetTicketByHash,
|
||||
"testSetTicketVotingKey": testSetTicketVotingKey,
|
||||
"testUpdateExpireAndFee": testUpdateExpireAndFee,
|
||||
"testUpdateVoteChoices": testUpdateVoteChoices,
|
||||
"testInsertNewTicket": testInsertNewTicket,
|
||||
"testGetTicketByHash": testGetTicketByHash,
|
||||
}
|
||||
|
||||
for testName, test := range tests {
|
||||
@ -64,23 +62,23 @@ func TestDatabase(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testInsertTicket(t *testing.T) {
|
||||
func testInsertNewTicket(t *testing.T) {
|
||||
// Insert a ticket into the database.
|
||||
ticket := exampleTicket()
|
||||
err := db.InsertTicket(ticket)
|
||||
err := db.InsertNewTicket(ticket)
|
||||
if err != nil {
|
||||
t.Fatalf("error storing ticket in database: %v", err)
|
||||
}
|
||||
|
||||
// Inserting a ticket with the same hash should fail.
|
||||
err = db.InsertTicket(ticket)
|
||||
err = db.InsertNewTicket(ticket)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error inserting ticket with duplicate hash")
|
||||
}
|
||||
|
||||
// Inserting a ticket with empty hash should fail.
|
||||
ticket.Hash = ""
|
||||
err = db.InsertTicket(ticket)
|
||||
err = db.InsertNewTicket(ticket)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error inserting ticket with no hash")
|
||||
}
|
||||
@ -89,7 +87,7 @@ func testInsertTicket(t *testing.T) {
|
||||
func testGetTicketByHash(t *testing.T) {
|
||||
ticket := exampleTicket()
|
||||
// Insert a ticket into the database.
|
||||
err := db.InsertTicket(ticket)
|
||||
err := db.InsertNewTicket(ticket)
|
||||
if err != nil {
|
||||
t.Fatalf("error storing ticket in database: %v", err)
|
||||
}
|
||||
@ -108,13 +106,14 @@ func testGetTicketByHash(t *testing.T) {
|
||||
retrieved.CommitmentAddress != ticket.CommitmentAddress ||
|
||||
retrieved.FeeAddressIndex != ticket.FeeAddressIndex ||
|
||||
retrieved.FeeAddress != ticket.FeeAddress ||
|
||||
retrieved.SDiff != ticket.SDiff ||
|
||||
retrieved.BlockHeight != ticket.BlockHeight ||
|
||||
!reflect.DeepEqual(retrieved.VoteChoices, ticket.VoteChoices) ||
|
||||
retrieved.VotingKey != ticket.VotingKey ||
|
||||
retrieved.VSPFee != ticket.VSPFee ||
|
||||
retrieved.FeeExpiration != ticket.FeeExpiration ||
|
||||
retrieved.Confirmed != ticket.Confirmed ||
|
||||
!reflect.DeepEqual(retrieved.VoteChoices, ticket.VoteChoices) ||
|
||||
retrieved.VotingWIF != ticket.VotingWIF ||
|
||||
retrieved.FeeTxHex != ticket.FeeTxHex ||
|
||||
retrieved.FeeTxHash != ticket.FeeTxHash ||
|
||||
retrieved.FeeExpiration != ticket.FeeExpiration {
|
||||
retrieved.FeeConfirmed != ticket.FeeConfirmed {
|
||||
t.Fatal("retrieved ticket value didnt match expected")
|
||||
}
|
||||
|
||||
@ -128,90 +127,7 @@ func testGetTicketByHash(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testSetTicketVotingKey(t *testing.T) {
|
||||
// Insert a ticket into the database.
|
||||
ticket := exampleTicket()
|
||||
err := db.InsertTicket(ticket)
|
||||
if err != nil {
|
||||
t.Fatalf("error storing ticket in database: %v", err)
|
||||
}
|
||||
// TODO: Add tests for UpdateTicket, CountTickets, GetUnconfirmedTickets,
|
||||
// GetPendingFees, GetUnconfirmedFees.
|
||||
|
||||
// Update values.
|
||||
newVotingKey := ticket.VotingKey + "2"
|
||||
newVoteChoices := ticket.VoteChoices
|
||||
feeTxHash := ticket.FeeTxHash + "3"
|
||||
newVoteChoices["AgendaID"] = "Different choice"
|
||||
err = db.SetTicketVotingKey(ticket.Hash, newVotingKey, newVoteChoices, feeTxHash)
|
||||
if err != nil {
|
||||
t.Fatalf("error updating votingkey and votechoices: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve ticket from database.
|
||||
retrieved, _, err := db.GetTicketByHash(ticket.Hash)
|
||||
if err != nil {
|
||||
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
|
||||
}
|
||||
|
||||
// Check ticket fields match expected.
|
||||
if !reflect.DeepEqual(newVoteChoices, retrieved.VoteChoices) ||
|
||||
feeTxHash != retrieved.FeeTxHash ||
|
||||
newVotingKey != retrieved.VotingKey {
|
||||
t.Fatal("retrieved ticket value didnt match expected")
|
||||
}
|
||||
}
|
||||
|
||||
func testUpdateExpireAndFee(t *testing.T) {
|
||||
// Insert a ticket into the database.
|
||||
ticket := exampleTicket()
|
||||
err := db.InsertTicket(ticket)
|
||||
if err != nil {
|
||||
t.Fatalf("error storing ticket in database: %v", err)
|
||||
}
|
||||
|
||||
// Update ticket with new values.
|
||||
newExpiry := ticket.FeeExpiration + 1
|
||||
newFee := ticket.VSPFee + 1
|
||||
err = db.UpdateExpireAndFee(ticket.Hash, newExpiry, newFee)
|
||||
if err != nil {
|
||||
t.Fatalf("error updating expiry and fee: %v", err)
|
||||
}
|
||||
|
||||
// Get updated ticket
|
||||
retrieved, _, err := db.GetTicketByHash(ticket.Hash)
|
||||
if err != nil {
|
||||
t.Fatalf("error retrieving updated ticket: %v", err)
|
||||
}
|
||||
|
||||
// Check ticket fields match expected.
|
||||
if retrieved.VSPFee != newFee || retrieved.FeeExpiration != newExpiry {
|
||||
t.Fatal("retrieved ticket value didnt match expected")
|
||||
}
|
||||
}
|
||||
|
||||
func testUpdateVoteChoices(t *testing.T) {
|
||||
// Insert a ticket into the database.
|
||||
ticket := exampleTicket()
|
||||
err := db.InsertTicket(ticket)
|
||||
if err != nil {
|
||||
t.Fatalf("error storing ticket in database: %v", err)
|
||||
}
|
||||
|
||||
// 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 votechoices: %v", err)
|
||||
}
|
||||
|
||||
// Get updated ticket
|
||||
retrieved, _, err := db.GetTicketByHash(ticket.Hash)
|
||||
if err != nil {
|
||||
t.Fatalf("error retrieving updated ticket: %v", err)
|
||||
}
|
||||
|
||||
// Check ticket fields match expected.
|
||||
if !reflect.DeepEqual(newVoteChoices, retrieved.VoteChoices) {
|
||||
t.Fatal("retrieved ticket value didnt match expected")
|
||||
}
|
||||
}
|
||||
// TODO: Add tests for ticket.FeeExpired.
|
||||
|
||||
@ -9,18 +9,32 @@ import (
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
// TODO: Properly document ticket lifecycle.
|
||||
// TODO: Shorten json keys, they are stored in the db and duplicated many times.
|
||||
|
||||
type Ticket struct {
|
||||
Hash string `json:"hash"`
|
||||
CommitmentAddress string `json:"commitmentaddress"`
|
||||
FeeAddressIndex uint32 `json:"feeaddressindex"`
|
||||
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"`
|
||||
FeeExpiration int64 `json:"feeexpiration"`
|
||||
FeeTxHash string `json:"feetxhash"`
|
||||
Hash string `json:"hash"`
|
||||
CommitmentAddress string `json:"commitmentaddress"`
|
||||
FeeAddressIndex uint32 `json:"feeaddressindex"`
|
||||
FeeAddress string `json:"feeaddress"`
|
||||
VSPFee float64 `json:"vspfee"`
|
||||
FeeExpiration int64 `json:"feeexpiration"`
|
||||
|
||||
// Confirmed will be set when the ticket has 6+ confirmations.
|
||||
Confirmed bool `json:"confirmed"`
|
||||
|
||||
// VoteChoices and VotingWIF are set in /payfee.
|
||||
VoteChoices map[string]string `json:"votechoices"`
|
||||
VotingWIF string `json:"votingwif"`
|
||||
|
||||
// FeeTxHex will be set when the fee tx has been received from the user.
|
||||
FeeTxHex string `json:"feetxhex"`
|
||||
|
||||
// FeeTxHash will be set when the fee tx has been broadcast.
|
||||
FeeTxHash string `json:"feetxhash"`
|
||||
|
||||
// FeeConfirmed will be set when the fee tx has 6+ confirmations.
|
||||
FeeConfirmed bool `json:"feeconfirmed"`
|
||||
}
|
||||
|
||||
func (t *Ticket) FeeExpired() bool {
|
||||
@ -32,11 +46,12 @@ var (
|
||||
ErrNoTicketFound = errors.New("no ticket found")
|
||||
)
|
||||
|
||||
func (vdb *VspDatabase) InsertTicket(ticket Ticket) error {
|
||||
hashBytes := []byte(ticket.Hash)
|
||||
func (vdb *VspDatabase) InsertNewTicket(ticket Ticket) error {
|
||||
return vdb.db.Update(func(tx *bolt.Tx) error {
|
||||
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
||||
|
||||
hashBytes := []byte(ticket.Hash)
|
||||
|
||||
if ticketBkt.Get(hashBytes) != nil {
|
||||
return fmt.Errorf("ticket already exists with hash %s", ticket.Hash)
|
||||
}
|
||||
@ -45,35 +60,26 @@ func (vdb *VspDatabase) InsertTicket(ticket Ticket) error {
|
||||
|
||||
ticketBytes, err := json.Marshal(ticket)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("could not marshal ticket: %v", err)
|
||||
}
|
||||
|
||||
return ticketBkt.Put(hashBytes, ticketBytes)
|
||||
})
|
||||
}
|
||||
|
||||
func (vdb *VspDatabase) SetTicketVotingKey(ticketHash, votingKey string, voteChoices map[string]string, feeTxHash string) error {
|
||||
func (vdb *VspDatabase) UpdateTicket(ticket Ticket) error {
|
||||
return vdb.db.Update(func(tx *bolt.Tx) error {
|
||||
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
||||
|
||||
hashBytes := []byte(ticketHash)
|
||||
hashBytes := []byte(ticket.Hash)
|
||||
|
||||
ticketBytes := ticketBkt.Get(hashBytes)
|
||||
if ticketBytes == nil {
|
||||
return ErrNoTicketFound
|
||||
if ticketBkt.Get(hashBytes) == nil {
|
||||
return fmt.Errorf("ticket does not exist with hash %s", ticket.Hash)
|
||||
}
|
||||
|
||||
var ticket Ticket
|
||||
err := json.Unmarshal(ticketBytes, &ticket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not unmarshal ticket: %v", err)
|
||||
}
|
||||
// TODO: Error if a ticket already exists with the same fee address.
|
||||
|
||||
ticket.VotingKey = votingKey
|
||||
ticket.VoteChoices = voteChoices
|
||||
ticket.FeeTxHash = feeTxHash
|
||||
|
||||
ticketBytes, err = json.Marshal(ticket)
|
||||
ticketBytes, err := json.Marshal(ticket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal ticket: %v", err)
|
||||
}
|
||||
@ -106,59 +112,6 @@ func (vdb *VspDatabase) GetTicketByHash(ticketHash string) (Ticket, bool, error)
|
||||
return ticket, found, err
|
||||
}
|
||||
|
||||
func (vdb *VspDatabase) UpdateVoteChoices(ticketHash string, voteChoices map[string]string) error {
|
||||
return vdb.db.Update(func(tx *bolt.Tx) error {
|
||||
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
||||
hashBytes := []byte(ticketHash)
|
||||
|
||||
ticketBytes := ticketBkt.Get(hashBytes)
|
||||
if ticketBytes == nil {
|
||||
return ErrNoTicketFound
|
||||
}
|
||||
|
||||
var ticket Ticket
|
||||
err := json.Unmarshal(ticketBytes, &ticket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not unmarshal ticket: %v", err)
|
||||
}
|
||||
ticket.VoteChoices = voteChoices
|
||||
|
||||
ticketBytes, err = json.Marshal(ticket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal ticket: %v", err)
|
||||
}
|
||||
|
||||
return ticketBkt.Put(hashBytes, ticketBytes)
|
||||
})
|
||||
}
|
||||
|
||||
func (vdb *VspDatabase) UpdateExpireAndFee(ticketHash string, expiration int64, vspFee float64) error {
|
||||
return vdb.db.Update(func(tx *bolt.Tx) error {
|
||||
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
||||
hashBytes := []byte(ticketHash)
|
||||
|
||||
ticketBytes := ticketBkt.Get(hashBytes)
|
||||
if ticketBytes == nil {
|
||||
return ErrNoTicketFound
|
||||
}
|
||||
|
||||
var ticket Ticket
|
||||
err := json.Unmarshal(ticketBytes, &ticket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not unmarshal ticket: %v", err)
|
||||
}
|
||||
ticket.FeeExpiration = expiration
|
||||
ticket.VSPFee = vspFee
|
||||
|
||||
ticketBytes, err = json.Marshal(ticket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal ticket: %v", err)
|
||||
}
|
||||
|
||||
return ticketBkt.Put(hashBytes, ticketBytes)
|
||||
})
|
||||
}
|
||||
|
||||
func (vdb *VspDatabase) CountTickets() (int, int, error) {
|
||||
var total, feePaid int
|
||||
err := vdb.db.View(func(tx *bolt.Tx) error {
|
||||
@ -182,3 +135,78 @@ func (vdb *VspDatabase) CountTickets() (int, int, error) {
|
||||
|
||||
return total, feePaid, err
|
||||
}
|
||||
|
||||
func (vdb *VspDatabase) GetUnconfirmedTickets() ([]Ticket, error) {
|
||||
var tickets []Ticket
|
||||
err := vdb.db.View(func(tx *bolt.Tx) error {
|
||||
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
||||
|
||||
return ticketBkt.ForEach(func(k, v []byte) error {
|
||||
var ticket Ticket
|
||||
err := json.Unmarshal(v, &ticket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not unmarshal ticket: %v", err)
|
||||
}
|
||||
|
||||
if !ticket.Confirmed {
|
||||
tickets = append(tickets, ticket)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
return tickets, err
|
||||
}
|
||||
|
||||
func (vdb *VspDatabase) GetPendingFees() ([]Ticket, error) {
|
||||
var tickets []Ticket
|
||||
err := vdb.db.View(func(tx *bolt.Tx) error {
|
||||
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
||||
|
||||
return ticketBkt.ForEach(func(k, v []byte) error {
|
||||
var ticket Ticket
|
||||
err := json.Unmarshal(v, &ticket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not unmarshal ticket: %v", err)
|
||||
}
|
||||
|
||||
// Add ticket if it is confirmed, and we have a fee tx, and the tx
|
||||
// is not broadcast yet.
|
||||
if ticket.Confirmed &&
|
||||
ticket.FeeTxHex != "" &&
|
||||
ticket.FeeTxHash == "" {
|
||||
tickets = append(tickets, ticket)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
return tickets, err
|
||||
}
|
||||
|
||||
func (vdb *VspDatabase) GetUnconfirmedFees() ([]Ticket, error) {
|
||||
var tickets []Ticket
|
||||
err := vdb.db.View(func(tx *bolt.Tx) error {
|
||||
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)
|
||||
|
||||
return ticketBkt.ForEach(func(k, v []byte) error {
|
||||
var ticket Ticket
|
||||
err := json.Unmarshal(v, &ticket)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not unmarshal ticket: %v", err)
|
||||
}
|
||||
|
||||
// Add ticket if fee tx is broadcast but not confirmed yet.
|
||||
if ticket.FeeTxHash != "" &&
|
||||
!ticket.FeeConfirmed {
|
||||
tickets = append(tickets, ticket)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
return tickets, err
|
||||
}
|
||||
|
||||
11
go.mod
11
go.mod
@ -4,14 +4,13 @@ go 1.13
|
||||
|
||||
require (
|
||||
decred.org/dcrwallet v1.2.3-0.20200519180100-f1aa4c354e05
|
||||
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200511175520-d08cb3f72b3b
|
||||
github.com/decred/dcrd/chaincfg/chainhash v1.0.2
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200511175520-d08cb3f72b3b
|
||||
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200522182228-0c7cbb53680b
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200522182228-0c7cbb53680b
|
||||
github.com/decred/dcrd/dcrec v1.0.0
|
||||
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200517213104-6ade94486839
|
||||
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200421213827-b60c60ffe98b
|
||||
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200522182228-0c7cbb53680b
|
||||
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200522182228-0c7cbb53680b
|
||||
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0
|
||||
github.com/decred/dcrd/txscript/v3 v3.0.0-20200511175520-d08cb3f72b3b
|
||||
github.com/decred/dcrd/txscript/v3 v3.0.0-20200522182228-0c7cbb53680b
|
||||
github.com/decred/dcrd/wire v1.3.0
|
||||
github.com/decred/slog v1.0.0
|
||||
github.com/gin-gonic/gin v1.6.3
|
||||
|
||||
24
go.sum
24
go.sum
@ -14,6 +14,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4=
|
||||
github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
|
||||
github.com/decred/base58 v1.0.1/go.mod h1:H2ENcsJjye1G7CbRa67kV9OFaui0LGr56ntKKoY5g9c=
|
||||
github.com/decred/base58 v1.0.2 h1:yupIH6bg+q7KYfBk7oUv3xFjKGb5Ypm4+v/61X4keGY=
|
||||
@ -22,9 +23,8 @@ github.com/decred/dcrd/addrmgr v1.1.0/go.mod h1:exghL+0+QeVvO4MXezWJ1C2tcpBn3ngf
|
||||
github.com/decred/dcrd/blockchain/stake/v2 v2.0.2/go.mod h1:o2TT/l/YFdrt15waUdlZ3g90zfSwlA0WgQqHV9UGJF4=
|
||||
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:aDL94kcVJfaaJP+acWUJrlK7g7xEOqTSiFe6bSN3yRQ=
|
||||
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU=
|
||||
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200511175520-d08cb3f72b3b h1:8ChbBKdGbsfAUVWwqUzZIbGHg1z0YpFrVokpNETpal0=
|
||||
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200511175520-d08cb3f72b3b/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU=
|
||||
github.com/decred/dcrd/blockchain/standalone v1.1.0 h1:yclvVGEY09Gf8A4GSAo+NCtL1dW2TYJ4OKp4+g0ICI0=
|
||||
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200522182228-0c7cbb53680b h1:lpObaxbwkkC7ZZDkqJnL7Rj64cBGOdhVELjYcYtyAAA=
|
||||
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200522182228-0c7cbb53680b/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU=
|
||||
github.com/decred/dcrd/blockchain/standalone v1.1.0/go.mod h1:6K8ZgzlWM1Kz2TwXbrtiAvfvIwfAmlzrtpA7CVPCUPE=
|
||||
github.com/decred/dcrd/blockchain/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:R9rIXU8kEJVC9Z4LAlh9bo9hiT3a+ihys3mCrz4PVao=
|
||||
github.com/decred/dcrd/certgen v1.1.0/go.mod h1:ivkPLChfjdAgFh7ZQOtl6kJRqVkfrCq67dlq3AbZBQE=
|
||||
@ -37,8 +37,8 @@ github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215015031-3283587e6add/go.mod h1:
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200511175520-d08cb3f72b3b h1:L6qM+5ISaaYSnNMAFMouu/FGcIN0tP42rJUdtAJqKgY=
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200511175520-d08cb3f72b3b/go.mod h1:OHbKBa6UZZOXCU1Y8f9Ta3O+GShto7nB1O0nuEutKq4=
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200522182228-0c7cbb53680b h1:pWbJdg3FFix4W4acIkkQuvBp2t1n8PZPSb4uFMxk8wI=
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200522182228-0c7cbb53680b/go.mod h1:OHbKBa6UZZOXCU1Y8f9Ta3O+GShto7nB1O0nuEutKq4=
|
||||
github.com/decred/dcrd/connmgr/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:mvIMJsrOEngogmVrq+tdbPIZchHVgGnVBZeNwj1cW6E=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||
@ -66,20 +66,22 @@ github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215015031-3283587e6add/go.mod h1:C
|
||||
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:48ZLpNNrRIYfqYxmvzMgOZrnTZUU3aTJveWtamCkOxo=
|
||||
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:jFxEd2LWDLvrWlrIiyx9ZGTQjvoFHZ0OVfBdyIX7jSw=
|
||||
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:/CDBC1SOXKrmihavgXviaTr6eVZSAWKQqEbRmacDxgg=
|
||||
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200517213104-6ade94486839 h1:1re/l3jvxknGdZ9K0X5k4FbI0al+nfs09F0acpZEyTM=
|
||||
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200517213104-6ade94486839/go.mod h1:85NtF/fmqL2UDf0/gLhTHG/m/0HQHwG+erQKkwWW27A=
|
||||
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200522182228-0c7cbb53680b h1:eqcKN5ZcXshnTCG3GSAM+J1fxuKyD/ZR7SFg/cXB0eQ=
|
||||
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200522182228-0c7cbb53680b/go.mod h1:85NtF/fmqL2UDf0/gLhTHG/m/0HQHwG+erQKkwWW27A=
|
||||
github.com/decred/dcrd/gcs/v2 v2.0.0/go.mod h1:3XjKcrtvB+r2ezhIsyNCLk6dRnXRJVyYmsd1P3SkU3o=
|
||||
github.com/decred/dcrd/gcs/v2 v2.0.2-0.20200312171759-0a8cc56a776e h1:tBOk2P8F9JyRUSp0iRTs4nYEBro1FKBDIbg/UualLWw=
|
||||
github.com/decred/dcrd/gcs/v2 v2.0.2-0.20200312171759-0a8cc56a776e/go.mod h1:JJGd1m0DrFgV4J2J8HKNB9YVkM06ewQHT6iINis39Z4=
|
||||
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200421213827-b60c60ffe98b h1:pfhggbZaR/h4mjHwMMgCl4+UnstAssVA7FEezvMKCQo=
|
||||
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM=
|
||||
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200522182228-0c7cbb53680b h1:V9NS+FoeH6DbTeUt1tsMCO4CI4nfFvw1qi6DDXniufc=
|
||||
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200522182228-0c7cbb53680b/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM=
|
||||
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0 h1:uyvwjO+90KHxZIIztobB9cG+qVSHhCT+aGSiZF1vGAg=
|
||||
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0/go.mod h1:c5S+PtQWNIA2aUakgrLhrlopkMadcOv51dWhCEdo49c=
|
||||
github.com/decred/dcrd/txscript/v2 v2.1.0/go.mod h1:XaJAVrZU4NWRx4UEzTiDAs86op1m8GRJLz24SDBKOi0=
|
||||
github.com/decred/dcrd/txscript/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:ATMA8K0SOo+M9Wdbr6dMnAd8qICJi6pXjGLlKsJc99E=
|
||||
github.com/decred/dcrd/txscript/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:KsDS7McU1yFaCYR9LCIwk6YnE15YN3wJUDxhKdFqlsc=
|
||||
github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
|
||||
github.com/decred/dcrd/txscript/v3 v3.0.0-20200511175520-d08cb3f72b3b h1:PbEqUN+q0hg/TJdi2IQ0Y/5Qc8GNFrnioXeBXm060n8=
|
||||
github.com/decred/dcrd/txscript/v3 v3.0.0-20200511175520-d08cb3f72b3b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
|
||||
github.com/decred/dcrd/txscript/v3 v3.0.0-20200522182228-0c7cbb53680b h1:RHdkVPLSryp1SS+ig0vNpwAfsLKDn22sQXmr7KyH0eE=
|
||||
github.com/decred/dcrd/txscript/v3 v3.0.0-20200522182228-0c7cbb53680b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
|
||||
github.com/decred/dcrd/wire v1.3.0 h1:X76I2/a8esUmxXmFpJpAvXEi014IA4twgwcOBeIS8lE=
|
||||
github.com/decred/dcrd/wire v1.3.0/go.mod h1:fnKGlUY2IBuqnpxx5dYRU5Oiq392OBqAuVjRVSkIoXM=
|
||||
github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0=
|
||||
@ -115,6 +117,7 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jrick/bitset v1.0.0 h1:Ws0PXV3PwXqWK2n7Vz6idCdrV/9OrBXgHEJi27ZB9Dw=
|
||||
github.com/jrick/bitset v1.0.0/go.mod h1:ZOYB5Uvkla7wIEY4FEssPVi3IQXa02arznRaYaAEPe4=
|
||||
github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
@ -167,6 +170,7 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
||||
4
log.go
4
log.go
@ -8,6 +8,7 @@ import (
|
||||
"github.com/decred/slog"
|
||||
"github.com/jrick/logrotate/rotator"
|
||||
|
||||
"github.com/jholdstock/dcrvsp/background"
|
||||
"github.com/jholdstock/dcrvsp/database"
|
||||
"github.com/jholdstock/dcrvsp/rpc"
|
||||
"github.com/jholdstock/dcrvsp/webapi"
|
||||
@ -42,6 +43,7 @@ var (
|
||||
|
||||
log = backendLog.Logger("VSP")
|
||||
dbLog = backendLog.Logger(" DB")
|
||||
bgLog = backendLog.Logger(" BG")
|
||||
apiLog = backendLog.Logger("API")
|
||||
rpcLog = backendLog.Logger("RPC")
|
||||
)
|
||||
@ -49,6 +51,7 @@ var (
|
||||
// Initialize package-global logger variables.
|
||||
func init() {
|
||||
database.UseLogger(dbLog)
|
||||
background.UseLogger(bgLog)
|
||||
webapi.UseLogger(apiLog)
|
||||
rpc.UseLogger(rpcLog)
|
||||
}
|
||||
@ -57,6 +60,7 @@ func init() {
|
||||
var subsystemLoggers = map[string]slog.Logger{
|
||||
"VSP": log,
|
||||
" DB": dbLog,
|
||||
" BG": bgLog,
|
||||
"API": apiLog,
|
||||
"RPC": rpcLog,
|
||||
}
|
||||
|
||||
21
main.go
21
main.go
@ -8,6 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jholdstock/dcrvsp/background"
|
||||
"github.com/jholdstock/dcrvsp/database"
|
||||
"github.com/jholdstock/dcrvsp/rpc"
|
||||
"github.com/jholdstock/dcrvsp/webapi"
|
||||
@ -59,7 +60,8 @@ func run(ctx context.Context) error {
|
||||
// Create RPC client for local dcrd instance (used for broadcasting and
|
||||
// checking the status of fee transactions).
|
||||
// Dial once just to validate config.
|
||||
dcrdConnect := rpc.Setup(ctx, &shutdownWg, cfg.DcrdUser, cfg.DcrdPass, cfg.DcrdHost, cfg.dcrdCert)
|
||||
dcrdConnect := rpc.Setup(ctx, &shutdownWg, cfg.DcrdUser, cfg.DcrdPass,
|
||||
cfg.DcrdHost, cfg.dcrdCert, nil)
|
||||
dcrdConn, err := dcrdConnect()
|
||||
if err != nil {
|
||||
log.Errorf("dcrd connection error: %v", err)
|
||||
@ -77,7 +79,8 @@ func run(ctx context.Context) error {
|
||||
|
||||
// Create RPC client for remote dcrwallet instance (used for voting).
|
||||
// Dial once just to validate config.
|
||||
walletConnect := rpc.Setup(ctx, &shutdownWg, cfg.WalletUser, cfg.WalletPass, cfg.WalletHost, cfg.walletCert)
|
||||
walletConnect := rpc.Setup(ctx, &shutdownWg, cfg.WalletUser, cfg.WalletPass,
|
||||
cfg.WalletHost, cfg.walletCert, nil)
|
||||
walletConn, err := walletConnect()
|
||||
if err != nil {
|
||||
log.Errorf("dcrwallet connection error: %v", err)
|
||||
@ -93,6 +96,20 @@ func run(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a dcrd client with an attached notification handler which will run
|
||||
// in the background.
|
||||
notifHandler := &background.NotificationHandler{
|
||||
Ctx: ctx,
|
||||
Db: db,
|
||||
WalletConnect: walletConnect,
|
||||
}
|
||||
dcrdWithNotifHandler := rpc.Setup(ctx, &shutdownWg, cfg.DcrdUser, cfg.DcrdPass,
|
||||
cfg.DcrdHost, cfg.dcrdCert, notifHandler)
|
||||
|
||||
// Start background process which will continually attempt to reconnect to
|
||||
// dcrd if the connection drops.
|
||||
background.Start(notifHandler, dcrdWithNotifHandler)
|
||||
|
||||
// TODO: This can move into webapi.Start()
|
||||
signKey, pubKey, err := db.KeyPair()
|
||||
if err != nil {
|
||||
|
||||
@ -26,7 +26,7 @@ type Connect func() (Caller, error)
|
||||
// function which can be called to access the client. The returned function will
|
||||
// try to handle any client disconnects by attempting to reconnect, but will
|
||||
// return an error if a new connection cannot be established.
|
||||
func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr string, cert []byte) Connect {
|
||||
func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr string, cert []byte, n wsrpc.Notifier) Connect {
|
||||
|
||||
// Create TLS options.
|
||||
pool := x509.NewCertPool()
|
||||
@ -54,7 +54,6 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str
|
||||
log.Debugf("RPC already closed (%s)", addr)
|
||||
|
||||
default:
|
||||
log.Debugf("Closing RPC (%s)...", addr)
|
||||
if err := c.Close(); err != nil {
|
||||
log.Errorf("Failed to close RPC (%s): %v", addr, err)
|
||||
} else {
|
||||
@ -72,7 +71,7 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str
|
||||
if c != nil {
|
||||
select {
|
||||
case <-c.Done():
|
||||
log.Infof("RPC client errored (%v); reconnecting...", c.Err())
|
||||
log.Debugf("RPC client errored (%v); reconnecting...", c.Err())
|
||||
c = nil
|
||||
default:
|
||||
return c, nil
|
||||
@ -80,7 +79,7 @@ func Setup(ctx context.Context, shutdownWg *sync.WaitGroup, user, pass, addr str
|
||||
}
|
||||
|
||||
var err error
|
||||
c, err = wsrpc.Dial(ctx, fullAddr, tlsOpt, authOpt)
|
||||
c, err = wsrpc.Dial(ctx, fullAddr, tlsOpt, authOpt, wsrpc.WithNotifier(n))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
27
rpc/dcrd.go
27
rpc/dcrd.go
@ -3,6 +3,7 @@ package rpc
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/decred/dcrd/blockchain/stake/v3"
|
||||
@ -45,16 +46,6 @@ func DcrdClient(ctx context.Context, c Caller) (*DcrdRPC, error) {
|
||||
return &DcrdRPC{c, ctx}, nil
|
||||
}
|
||||
|
||||
func (c *DcrdRPC) GetBlockHeader(blockHash string) (*dcrdtypes.GetBlockHeaderVerboseResult, error) {
|
||||
verbose := true
|
||||
var blockHeader dcrdtypes.GetBlockHeaderVerboseResult
|
||||
err := c.Call(c.ctx, "getblockheader", &blockHeader, blockHash, verbose)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &blockHeader, nil
|
||||
}
|
||||
|
||||
func (c *DcrdRPC) GetRawTransaction(txHash string) (*dcrdtypes.TxRawResult, error) {
|
||||
verbose := 1
|
||||
var resp dcrdtypes.TxRawResult
|
||||
@ -76,11 +67,11 @@ func (c *DcrdRPC) SendRawTransaction(txHex string) (string, error) {
|
||||
}
|
||||
|
||||
func (c *DcrdRPC) GetTicketCommitmentAddress(ticketHash string, netParams *chaincfg.Params) (string, error) {
|
||||
// Retrieve and parse the transaction.
|
||||
resp, err := c.GetRawTransaction(ticketHash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
msgHex, err := hex.DecodeString(resp.Hex)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -89,6 +80,16 @@ func (c *DcrdRPC) GetTicketCommitmentAddress(ticketHash string, netParams *chain
|
||||
if err = msgTx.FromBytes(msgHex); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Ensure transaction is a valid ticket.
|
||||
if !stake.IsSStx(msgTx) {
|
||||
return "", errors.New("invalid transcation - not sstx")
|
||||
}
|
||||
if len(msgTx.TxOut) != 3 {
|
||||
return "", fmt.Errorf("invalid transcation - expected 3 outputs, got %d", len(msgTx.TxOut))
|
||||
}
|
||||
|
||||
// Get ticket commitment address.
|
||||
addr, err := stake.AddrFromSStxPkScrCommitment(msgTx.TxOut[1].PkScript, netParams)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -96,3 +97,7 @@ func (c *DcrdRPC) GetTicketCommitmentAddress(ticketHash string, netParams *chain
|
||||
|
||||
return addr.Address(), nil
|
||||
}
|
||||
|
||||
func (c *DcrdRPC) NotifyBlocks() error {
|
||||
return c.Call(c.ctx, "notifyblocks", nil)
|
||||
}
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/decred/dcrd/blockchain/stake/v3"
|
||||
"github.com/decred/dcrd/wire"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/jholdstock/dcrvsp/database"
|
||||
@ -59,15 +56,13 @@ func feeAddress(c *gin.Context) {
|
||||
if knownTicket {
|
||||
// If the expiry period has passed we need to issue a new fee.
|
||||
now := time.Now()
|
||||
expire := ticket.FeeExpiration
|
||||
VSPFee := ticket.VSPFee
|
||||
if now.After(time.Unix(ticket.FeeExpiration, 0)) {
|
||||
expire = now.Add(cfg.FeeAddressExpiration).Unix()
|
||||
VSPFee = cfg.VSPFee
|
||||
if ticket.FeeExpired() {
|
||||
ticket.FeeExpiration = now.Add(cfg.FeeAddressExpiration).Unix()
|
||||
ticket.VSPFee = cfg.VSPFee
|
||||
|
||||
err := db.UpdateExpireAndFee(ticket.Hash, expire, VSPFee)
|
||||
err := db.UpdateTicket(ticket)
|
||||
if err != nil {
|
||||
log.Errorf("UpdateExpireAndFee error: %v", err)
|
||||
log.Errorf("UpdateTicket error: %v", err)
|
||||
sendErrorResponse("database error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
@ -76,8 +71,8 @@ func feeAddress(c *gin.Context) {
|
||||
Timestamp: now.Unix(),
|
||||
Request: feeAddressRequest,
|
||||
FeeAddress: ticket.FeeAddress,
|
||||
Fee: VSPFee,
|
||||
Expiration: expire,
|
||||
Fee: ticket.VSPFee,
|
||||
Expiration: ticket.FeeExpiration,
|
||||
}, c)
|
||||
|
||||
return
|
||||
@ -88,56 +83,25 @@ func feeAddress(c *gin.Context) {
|
||||
|
||||
ticketHash := feeAddressRequest.TicketHash
|
||||
|
||||
// Ensure ticket exists and is mined.
|
||||
resp, err := dcrdClient.GetRawTransaction(ticketHash)
|
||||
// Get transaction details.
|
||||
rawTx, err := dcrdClient.GetRawTransaction(ticketHash)
|
||||
if err != nil {
|
||||
log.Warnf("Could not retrieve tx %s for %s: %v", ticketHash, c.ClientIP(), err)
|
||||
sendErrorResponse("unknown transaction", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
if resp.Confirmations < 2 || resp.BlockHeight < 0 {
|
||||
log.Warnf("Not enough confs for tx from %s", c.ClientIP())
|
||||
sendErrorResponse("transaction does not have minimum confirmations", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
if resp.Confirmations > int64(uint32(cfg.NetParams.TicketMaturity)+cfg.NetParams.TicketExpiry) {
|
||||
|
||||
// Don't accept tickets which are too old.
|
||||
if rawTx.Confirmations > int64(uint32(cfg.NetParams.TicketMaturity)+cfg.NetParams.TicketExpiry) {
|
||||
log.Warnf("Too old tx from %s", c.ClientIP())
|
||||
sendErrorResponse("transaction too old", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
msgHex, err := hex.DecodeString(resp.Hex)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to decode tx: %v", err)
|
||||
sendErrorResponse("unable to decode transaction", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
msgTx := wire.NewMsgTx()
|
||||
if err = msgTx.FromBytes(msgHex); err != nil {
|
||||
log.Errorf("Failed to deserialize tx: %v", err)
|
||||
sendErrorResponse("failed to deserialize transaction", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
if !stake.IsSStx(msgTx) {
|
||||
log.Warnf("Non-ticket tx from %s", c.ClientIP())
|
||||
sendErrorResponse("transaction is not a ticket", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
if len(msgTx.TxOut) != 3 {
|
||||
log.Warnf("Invalid ticket from %s", c.ClientIP())
|
||||
sendErrorResponse("invalid ticket", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// get blockheight and sdiff which is required by
|
||||
// txrules.StakePoolTicketFee, and store them in the database
|
||||
// for processing by payfee
|
||||
blockHeader, err := dcrdClient.GetBlockHeader(resp.BlockHash)
|
||||
if err != nil {
|
||||
log.Errorf("GetBlockHeader error: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
// Check if ticket is fully confirmed.
|
||||
var confirmed bool
|
||||
if rawTx.Confirmations >= requiredConfs {
|
||||
confirmed = true
|
||||
}
|
||||
|
||||
newAddress, newAddressIdx, err := getNewFeeAddress(db, addrGen)
|
||||
@ -153,22 +117,21 @@ func feeAddress(c *gin.Context) {
|
||||
CommitmentAddress: commitmentAddress,
|
||||
FeeAddressIndex: newAddressIdx,
|
||||
FeeAddress: newAddress,
|
||||
SDiff: blockHeader.SBits,
|
||||
BlockHeight: int64(blockHeader.Height),
|
||||
Confirmed: confirmed,
|
||||
VSPFee: cfg.VSPFee,
|
||||
FeeExpiration: expire,
|
||||
// VotingKey and VoteChoices: set during payfee
|
||||
}
|
||||
|
||||
err = db.InsertTicket(dbTicket)
|
||||
err = db.InsertNewTicket(dbTicket)
|
||||
if err != nil {
|
||||
log.Errorf("InsertTicket error: %v", err)
|
||||
sendErrorResponse("database error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("Fee address created for new ticket: feeAddrIdx=%d, "+
|
||||
"feeAddr=%s, ticketHash=%s", newAddressIdx, newAddress, ticketHash)
|
||||
log.Debugf("Fee address created for new ticket: tktConfirmed=%t, feeAddrIdx=%d, "+
|
||||
"feeAddr=%s, ticketHash=%s", confirmed, newAddressIdx, newAddress, ticketHash)
|
||||
|
||||
sendJSONResponse(feeAddressResponse{
|
||||
Timestamp: now.Unix(),
|
||||
|
||||
@ -101,7 +101,7 @@ func vspAuth() gin.HandlerFunc {
|
||||
commitmentAddress, err = dcrdClient.GetTicketCommitmentAddress(hash, cfg.NetParams)
|
||||
if err != nil {
|
||||
log.Errorf("GetTicketCommitmentAddress error: %v", err)
|
||||
sendErrorResponse("database error", http.StatusInternalServerError, c)
|
||||
sendErrorResponse(err.Error(), http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
105
webapi/payfee.go
105
webapi/payfee.go
@ -1,12 +1,10 @@
|
||||
package webapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"decred.org/dcrwallet/wallet/txrules"
|
||||
"github.com/decred/dcrd/dcrec"
|
||||
"github.com/decred/dcrd/dcrutil/v3"
|
||||
"github.com/decred/dcrd/txscript/v3"
|
||||
@ -25,7 +23,6 @@ func payFee(c *gin.Context) {
|
||||
ticket := c.MustGet("Ticket").(database.Ticket)
|
||||
knownTicket := c.MustGet("KnownTicket").(bool)
|
||||
dcrdClient := c.MustGet("DcrdClient").(*rpc.DcrdRPC)
|
||||
walletClient := c.MustGet("WalletClient").(*rpc.WalletRPC)
|
||||
|
||||
if !knownTicket {
|
||||
log.Warnf("Invalid ticket from %s", c.ClientIP())
|
||||
@ -40,17 +37,22 @@ func payFee(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Respond early if fee transaction has already been broadcast for this
|
||||
// ticket.
|
||||
if ticket.FeeTxHash != "" {
|
||||
// Respond early if we already have the fee tx for this ticket.
|
||||
if ticket.FeeTxHex != "" {
|
||||
sendJSONResponse(payFeeResponse{
|
||||
Timestamp: time.Now().Unix(),
|
||||
TxHash: ticket.FeeTxHash,
|
||||
Request: payFeeRequest,
|
||||
}, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond early if the fee for this ticket is expired.
|
||||
if ticket.FeeExpired() {
|
||||
log.Warnf("Expired payfee request from %s", c.ClientIP())
|
||||
sendErrorResponse("fee has expired", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate VotingKey.
|
||||
votingKey := payFeeRequest.VotingKey
|
||||
votingWIF, err := dcrutil.DecodeWIF(votingKey, cfg.NetParams.PrivateKeyID)
|
||||
@ -85,21 +87,6 @@ func payFee(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
feeTxBuf := new(bytes.Buffer)
|
||||
feeTxBuf.Grow(feeTx.SerializeSize())
|
||||
err = feeTx.Serialize(feeTxBuf)
|
||||
if err != nil {
|
||||
log.Errorf("Serialize tx failed: %v", err)
|
||||
sendErrorResponse("serialize tx error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
if ticket.FeeExpired() {
|
||||
log.Warnf("Expired payfee request from %s", c.ClientIP())
|
||||
sendErrorResponse("fee has expired", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -138,79 +125,59 @@ findAddress:
|
||||
|
||||
// TODO: DB - validate votingkey against ticket submission address
|
||||
|
||||
sDiff := dcrutil.Amount(ticket.SDiff)
|
||||
|
||||
// TODO: Relay fee should not be hard coded
|
||||
relayFee, err := dcrutil.NewAmount(0.0001)
|
||||
minFee, err := dcrutil.NewAmount(cfg.VSPFee)
|
||||
if err != nil {
|
||||
log.Errorf("relayfee failed: %v", err)
|
||||
sendErrorResponse("relayfee error", http.StatusInternalServerError, c)
|
||||
log.Errorf("dcrutil.NewAmount: %v", err)
|
||||
sendErrorResponse("fee error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
minFee := txrules.StakePoolTicketFee(sDiff, relayFee, int32(ticket.BlockHeight), cfg.VSPFee, cfg.NetParams)
|
||||
if feeAmount < minFee {
|
||||
log.Errorf("Fee too small: was %v, expected %v", feeAmount, minFee)
|
||||
log.Warnf("Fee too small: was %v, expected %v", feeAmount, minFee)
|
||||
sendErrorResponse("fee too small", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
// At this point we are satisfied that the request is valid and the FeeTx
|
||||
// pays sufficient fees to the expected address.
|
||||
// Proceed to update the database and broadcast the transaction.
|
||||
// pays sufficient fees to the expected address. Proceed to update the
|
||||
// database, and if the ticket is confirmed broadcast the transaction.
|
||||
|
||||
feeTxHash, err := dcrdClient.SendRawTransaction(hex.EncodeToString(feeTxBuf.Bytes()))
|
||||
if err != nil {
|
||||
log.Errorf("SendRawTransaction failed: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
ticket.VotingWIF = votingWIF.String()
|
||||
ticket.FeeTxHex = payFeeRequest.FeeTx
|
||||
ticket.VoteChoices = voteChoices
|
||||
|
||||
err = db.SetTicketVotingKey(ticket.Hash, votingWIF.String(), voteChoices, feeTxHash)
|
||||
err = db.UpdateTicket(ticket)
|
||||
if err != nil {
|
||||
log.Errorf("SetTicketVotingKey failed: %v", err)
|
||||
log.Errorf("InsertTicket failed: %v", err)
|
||||
sendErrorResponse("database error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Should return a response here. We don't want to add the ticket to
|
||||
// the voting wallets until the fee tx has been confirmed.
|
||||
log.Debugf("Fee tx received for ticket: ticketHash=%s", ticket.Hash)
|
||||
|
||||
// Add ticket to voting wallets.
|
||||
rawTicket, err := dcrdClient.GetRawTransaction(ticket.Hash)
|
||||
if err != nil {
|
||||
log.Warnf("Could not retrieve tx %s for %s: %v", ticket.Hash, c.ClientIP(), err)
|
||||
sendErrorResponse("unknown transaction", http.StatusBadRequest, c)
|
||||
return
|
||||
}
|
||||
|
||||
err = walletClient.AddTransaction(rawTicket.BlockHash, rawTicket.Hex)
|
||||
if err != nil {
|
||||
log.Errorf("AddTransaction failed: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
err = walletClient.ImportPrivKey(votingWIF.String())
|
||||
if err != nil {
|
||||
log.Errorf("ImportPrivKey failed: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Update vote choices on voting wallets.
|
||||
for agenda, choice := range voteChoices {
|
||||
err = walletClient.SetVoteChoice(agenda, choice, ticket.Hash)
|
||||
if ticket.Confirmed {
|
||||
feeTxHash, err := dcrdClient.SendRawTransaction(payFeeRequest.FeeTx)
|
||||
if err != nil {
|
||||
log.Errorf("SetVoteChoice failed: %v", err)
|
||||
// TODO: SendRawTransaction can return a "transcation already
|
||||
// exists" error, which isnt necessarily a problem here.
|
||||
log.Errorf("SendRawTransaction failed: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
ticket.FeeTxHash = feeTxHash
|
||||
|
||||
err = db.UpdateTicket(ticket)
|
||||
if err != nil {
|
||||
log.Errorf("InsertTicket failed: %v", err)
|
||||
sendErrorResponse("database error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("Fee tx broadcast for ticket: ticketHash=%s", ticket.Hash)
|
||||
}
|
||||
|
||||
sendJSONResponse(payFeeResponse{
|
||||
Timestamp: time.Now().Unix(),
|
||||
TxHash: feeTxHash,
|
||||
Request: payFeeRequest,
|
||||
}, c)
|
||||
}
|
||||
|
||||
@ -42,23 +42,29 @@ func setVoteChoices(c *gin.Context) {
|
||||
|
||||
// Update VoteChoices in the database before updating the wallets. DB is
|
||||
// source of truth and is less likely to error.
|
||||
err = db.UpdateVoteChoices(ticket.Hash, voteChoices)
|
||||
ticket.VoteChoices = voteChoices
|
||||
err = db.UpdateTicket(ticket)
|
||||
if err != nil {
|
||||
log.Errorf("UpdateVoteChoices error: %v", err)
|
||||
log.Errorf("UpdateTicket error: %v", err)
|
||||
sendErrorResponse("database error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
|
||||
// Update vote choices on voting wallets.
|
||||
for agenda, choice := range voteChoices {
|
||||
err = walletClient.SetVoteChoice(agenda, choice, ticket.Hash)
|
||||
if err != nil {
|
||||
log.Errorf("SetVoteChoice failed: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
// Update vote choices on voting wallets. Tickets are only added to voting
|
||||
// wallets if their fee is confirmed.
|
||||
if ticket.FeeConfirmed {
|
||||
for agenda, choice := range voteChoices {
|
||||
err = walletClient.SetVoteChoice(agenda, choice, ticket.Hash)
|
||||
if err != nil {
|
||||
log.Errorf("SetVoteChoice failed: %v", err)
|
||||
sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Vote choices updated for ticket: ticketHash=%s", ticket.Hash)
|
||||
|
||||
// TODO: DB - error if given timestamp is older than any previous requests
|
||||
|
||||
// TODO: DB - store setvotechoices receipt in log
|
||||
|
||||
@ -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 }}</td></tr>
|
||||
<tr><td>VSP Fee:</td><td>{{ .VSPFee }} DCR per ticket</td></tr>
|
||||
<tr><td>Network:</td><td>{{ .Network }}</td></tr>
|
||||
</table>
|
||||
<p>Last updated: {{.UpdateTime}}</p>
|
||||
|
||||
@ -33,7 +33,6 @@ type PayFeeRequest struct {
|
||||
|
||||
type payFeeResponse struct {
|
||||
Timestamp int64 `json:"timestamp" binding:"required"`
|
||||
TxHash string `json:"txhash" binding:"required"`
|
||||
Request PayFeeRequest `json:"request" binding:"required"`
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +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 = 6
|
||||
)
|
||||
|
||||
var homepageData *gin.H
|
||||
|
||||
var cfg Config
|
||||
@ -168,13 +174,13 @@ func router(debugMode bool) *gin.Engine {
|
||||
)
|
||||
feeOnly.POST("/feeaddress", feeAddress)
|
||||
feeOnly.GET("/ticketstatus", ticketStatus)
|
||||
feeOnly.POST("/payfee", payFee)
|
||||
|
||||
// These API routes access dcrd and the voting wallets, and they need
|
||||
// authentication.
|
||||
both := router.Group("/api").Use(
|
||||
withDcrdClient(), withWalletClient(), vspAuth(),
|
||||
)
|
||||
both.POST("/payfee", payFee)
|
||||
both.POST("/setvotechoices", setVoteChoices)
|
||||
|
||||
return router
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user