decred.org/dcrwallet/v3@v3.1.0/internal/vsp/feepayment.go (about)

     1  package vsp
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	cryptorand "crypto/rand"
     7  	"encoding/hex"
     8  	"fmt"
     9  	"sync"
    10  	"time"
    11  
    12  	"decred.org/dcrwallet/v3/errors"
    13  	"decred.org/dcrwallet/v3/internal/uniformprng"
    14  	"decred.org/dcrwallet/v3/wallet"
    15  	"decred.org/dcrwallet/v3/wallet/txrules"
    16  	"decred.org/dcrwallet/v3/wallet/txsizes"
    17  	"github.com/decred/dcrd/blockchain/stake/v5"
    18  	"github.com/decred/dcrd/chaincfg/chainhash"
    19  	"github.com/decred/dcrd/chaincfg/v3"
    20  	"github.com/decred/dcrd/dcrutil/v4"
    21  	"github.com/decred/dcrd/txscript/v4"
    22  	"github.com/decred/dcrd/txscript/v4/stdaddr"
    23  	"github.com/decred/dcrd/txscript/v4/stdscript"
    24  	"github.com/decred/dcrd/wire"
    25  	"github.com/decred/vspd/types/v2"
    26  )
    27  
    28  var prng lockedRand
    29  
    30  type lockedRand struct {
    31  	mu   sync.Mutex
    32  	rand *uniformprng.Source
    33  }
    34  
    35  func (r *lockedRand) int63n(n int64) int64 {
    36  	r.mu.Lock()
    37  	defer r.mu.Unlock()
    38  	return r.rand.Int63n(n)
    39  }
    40  
    41  // duration returns a random time.Duration in [0,d) with uniform distribution.
    42  func (r *lockedRand) duration(d time.Duration) time.Duration {
    43  	return time.Duration(r.int63n(int64(d)))
    44  }
    45  
    46  func (r *lockedRand) coinflip() bool {
    47  	r.mu.Lock()
    48  	defer r.mu.Unlock()
    49  	return r.rand.Uint32n(2) == 0
    50  }
    51  
    52  func init() {
    53  	source, err := uniformprng.RandSource(cryptorand.Reader)
    54  	if err != nil {
    55  		panic(err)
    56  	}
    57  	prng = lockedRand{
    58  		rand: source,
    59  	}
    60  }
    61  
    62  var (
    63  	errStopped = errors.New("fee processing stopped")
    64  	errNotSolo = errors.New("not a solo ticket")
    65  )
    66  
    67  // A random amount of delay (between zero and these jitter constants) is added
    68  // before performing some background action with the VSP.  The delay is reduced
    69  // when a ticket is currently live, as it may be called to vote any time.
    70  const (
    71  	immatureJitter = time.Hour
    72  	liveJitter     = 5 * time.Minute
    73  	unminedJitter  = 2 * time.Minute
    74  )
    75  
    76  type feePayment struct {
    77  	client *Client
    78  	ctx    context.Context
    79  
    80  	// Set at feepayment creation and never changes
    81  	ticketHash     chainhash.Hash
    82  	commitmentAddr stdaddr.StakeAddress
    83  	votingAddr     stdaddr.StakeAddress
    84  	policy         Policy
    85  
    86  	// Requires locking for all access outside of Client.feePayment
    87  	mu            sync.Mutex
    88  	votingKey     string
    89  	ticketLive    int32
    90  	ticketExpires int32
    91  	fee           dcrutil.Amount
    92  	feeAddr       stdaddr.Address
    93  	feeHash       chainhash.Hash
    94  	feeTx         *wire.MsgTx
    95  	state         state
    96  	err           error
    97  
    98  	timerMu sync.Mutex
    99  	timer   *time.Timer
   100  }
   101  
   102  type state uint32
   103  
   104  const (
   105  	_ state = iota
   106  	unprocessed
   107  	feePublished
   108  	_ // ...
   109  	ticketSpent
   110  )
   111  
   112  func parseTicket(ticket *wire.MsgTx, params *chaincfg.Params) (
   113  	votingAddr, commitmentAddr stdaddr.StakeAddress, err error) {
   114  	fail := func(err error) (_, _ stdaddr.StakeAddress, _ error) {
   115  		return nil, nil, err
   116  	}
   117  	if !stake.IsSStx(ticket) {
   118  		return fail(fmt.Errorf("%v is not a ticket", ticket))
   119  	}
   120  	_, addrs := stdscript.ExtractAddrs(ticket.TxOut[0].Version, ticket.TxOut[0].PkScript, params)
   121  	if len(addrs) != 1 {
   122  		return fail(fmt.Errorf("cannot parse voting addr"))
   123  	}
   124  	switch addr := addrs[0].(type) {
   125  	case stdaddr.StakeAddress:
   126  		votingAddr = addr
   127  	default:
   128  		return fail(fmt.Errorf("address cannot be used for voting rights: %v", err))
   129  	}
   130  	commitmentAddr, err = stake.AddrFromSStxPkScrCommitment(ticket.TxOut[1].PkScript, params)
   131  	if err != nil {
   132  		return fail(fmt.Errorf("cannot parse commitment address: %w", err))
   133  	}
   134  	return
   135  }
   136  
   137  // calcHeights checks if the ticket has been mined, and if so, sets the live
   138  // height and expiry height fields. Should be called with mutex already held.
   139  func (fp *feePayment) calcHeights() {
   140  	_, minedHeight, err := fp.client.wallet.TxBlock(fp.ctx, &fp.ticketHash)
   141  	if err != nil {
   142  		// This is not expected to ever error, as the ticket has already been
   143  		// fetched from the wallet at least one before this point is reached.
   144  		log.Errorf("Failed to query block which mines ticket: %v", err)
   145  		return
   146  	}
   147  
   148  	if minedHeight < 2 {
   149  		return
   150  	}
   151  
   152  	params := fp.client.wallet.ChainParams()
   153  
   154  	// Note the off-by-one; this is correct. Tickets become live one block after
   155  	// the params would indicate.
   156  	fp.ticketLive = minedHeight + int32(params.TicketMaturity) + 1
   157  	fp.ticketExpires = fp.ticketLive + int32(params.TicketExpiry)
   158  }
   159  
   160  // expiryHeight returns the height at which the ticket expires. Returns zero if
   161  // the block is not yet mined. Should be called with mutex already held.
   162  func (fp *feePayment) expiryHeight() int32 {
   163  	if fp.ticketExpires == 0 {
   164  		fp.calcHeights()
   165  	}
   166  
   167  	return fp.ticketExpires
   168  }
   169  
   170  // liveHeight returns the height at which the ticket becomes live. Returns zero
   171  // if the block is not yet mined. Should be called with mutex already held.
   172  func (fp *feePayment) liveHeight() int32 {
   173  	if fp.ticketLive == 0 {
   174  		fp.calcHeights()
   175  	}
   176  
   177  	return fp.ticketLive
   178  }
   179  
   180  func (fp *feePayment) ticketSpent() bool {
   181  	ctx := fp.ctx
   182  	ticketOut := wire.OutPoint{Hash: fp.ticketHash, Index: 0, Tree: 1}
   183  	_, _, err := fp.client.wallet.Spender(ctx, &ticketOut)
   184  	return err == nil
   185  }
   186  
   187  func (fp *feePayment) ticketExpired() bool {
   188  	ctx := fp.ctx
   189  	w := fp.client.wallet
   190  	_, tipHeight := w.MainChainTip(ctx)
   191  
   192  	fp.mu.Lock()
   193  	expires := fp.expiryHeight()
   194  	fp.mu.Unlock()
   195  
   196  	return expires > 0 && tipHeight >= expires
   197  }
   198  
   199  func (fp *feePayment) removedExpiredOrSpent() bool {
   200  	var reason string
   201  	switch {
   202  	case fp.ticketExpired():
   203  		reason = "expired"
   204  	case fp.ticketSpent():
   205  		reason = "spent"
   206  	}
   207  	if reason != "" {
   208  		fp.remove(reason)
   209  		// nothing scheduled
   210  		return true
   211  	}
   212  	return false
   213  }
   214  
   215  func (fp *feePayment) remove(reason string) {
   216  	fp.stop()
   217  	log.Infof("ticket %v is %s; removing from VSP client", &fp.ticketHash, reason)
   218  	fp.client.mu.Lock()
   219  	delete(fp.client.jobs, fp.ticketHash)
   220  	fp.client.mu.Unlock()
   221  }
   222  
   223  // feePayment returns an existing managed fee payment, or creates and begins
   224  // processing a fee payment for a ticket.
   225  func (c *Client) feePayment(ctx context.Context, ticketHash *chainhash.Hash, paidConfirmed bool) (fp *feePayment) {
   226  	c.mu.Lock()
   227  	fp = c.jobs[*ticketHash]
   228  	c.mu.Unlock()
   229  	if fp != nil {
   230  		return fp
   231  	}
   232  
   233  	defer func() {
   234  		if fp == nil {
   235  			return
   236  		}
   237  		var schedule bool
   238  		c.mu.Lock()
   239  		fp2 := c.jobs[*ticketHash]
   240  		if fp2 != nil {
   241  			fp.stop()
   242  			fp = fp2
   243  		} else {
   244  			c.jobs[*ticketHash] = fp
   245  			schedule = true
   246  		}
   247  		c.mu.Unlock()
   248  		if schedule {
   249  			fp.schedule("reconcile payment", fp.reconcilePayment)
   250  		}
   251  	}()
   252  
   253  	w := c.wallet
   254  	params := w.ChainParams()
   255  
   256  	fp = &feePayment{
   257  		client:     c,
   258  		ctx:        context.Background(),
   259  		ticketHash: *ticketHash,
   260  		policy:     c.policy,
   261  	}
   262  
   263  	// No VSP interaction is required for spent tickets.
   264  	if fp.ticketSpent() {
   265  		fp.state = ticketSpent
   266  		return fp
   267  	}
   268  
   269  	ticket, err := c.tx(ctx, ticketHash)
   270  	if err != nil {
   271  		log.Warnf("no ticket found for %v", ticketHash)
   272  		return nil
   273  	}
   274  
   275  	fp.votingAddr, fp.commitmentAddr, err = parseTicket(ticket, params)
   276  	if err != nil {
   277  		log.Errorf("%v is not a ticket: %v", ticketHash, err)
   278  		return nil
   279  	}
   280  	// Try to access the voting key.
   281  	fp.votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr)
   282  	if err != nil {
   283  		log.Errorf("no voting key for ticket %v: %v", ticketHash, err)
   284  		return nil
   285  	}
   286  	feeHash, err := w.VSPFeeHashForTicket(ctx, ticketHash)
   287  	if err != nil {
   288  		// caller must schedule next method, as paying the fee may
   289  		// require using provided transaction inputs.
   290  		return fp
   291  	}
   292  
   293  	fee, err := c.tx(ctx, &feeHash)
   294  	if err != nil {
   295  		// A fee hash is recorded for this ticket, but was not found in
   296  		// the wallet.  This should not happen and may require manual
   297  		// intervention.
   298  		//
   299  		// XXX should check ticketinfo and see if fee is not paid. if
   300  		// possible, update it with a new fee.
   301  		fp.err = fmt.Errorf("fee transaction not found in wallet: %w", err)
   302  		return fp
   303  	}
   304  
   305  	fp.feeTx = fee
   306  	fp.feeHash = feeHash
   307  
   308  	// If database has been updated to paid or confirmed status, we can forgo
   309  	// this step.
   310  	if !paidConfirmed {
   311  		err = w.UpdateVspTicketFeeToStarted(ctx, ticketHash, &feeHash, c.Client.URL, c.Client.PubKey)
   312  		if err != nil {
   313  			return fp
   314  		}
   315  
   316  		fp.state = unprocessed // XXX fee created, but perhaps not submitted with vsp.
   317  		fp.fee = -1            // XXX fee amount (not needed anymore?)
   318  	}
   319  	return fp
   320  }
   321  
   322  func (c *Client) tx(ctx context.Context, hash *chainhash.Hash) (*wire.MsgTx, error) {
   323  	txs, _, err := c.wallet.GetTransactionsByHashes(ctx, []*chainhash.Hash{hash})
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  	return txs[0], nil
   328  }
   329  
   330  // Schedule a method to be executed.
   331  // Any currently-scheduled method is replaced.
   332  func (fp *feePayment) schedule(name string, method func() error) {
   333  	var delay time.Duration
   334  	if method != nil {
   335  		delay = fp.next()
   336  	}
   337  
   338  	fp.timerMu.Lock()
   339  	defer fp.timerMu.Unlock()
   340  	if fp.timer != nil {
   341  		fp.timer.Stop()
   342  		fp.timer = nil
   343  	}
   344  	if method != nil {
   345  		log.Debugf("scheduling %q for ticket %s in %v", name, &fp.ticketHash, delay)
   346  		fp.timer = time.AfterFunc(delay, fp.task(name, method))
   347  	}
   348  }
   349  
   350  func (fp *feePayment) next() time.Duration {
   351  	w := fp.client.wallet
   352  	params := w.ChainParams()
   353  	_, tipHeight := w.MainChainTip(fp.ctx)
   354  
   355  	fp.mu.Lock()
   356  	ticketLive := fp.liveHeight()
   357  	ticketExpires := fp.expiryHeight()
   358  	fp.mu.Unlock()
   359  
   360  	var jitter time.Duration
   361  	switch {
   362  	case tipHeight < ticketLive: // immature, mined ticket
   363  		blocksUntilLive := ticketLive - tipHeight
   364  		jitter = params.TargetTimePerBlock * time.Duration(blocksUntilLive)
   365  		if jitter > immatureJitter {
   366  			jitter = immatureJitter
   367  		}
   368  	case tipHeight < ticketExpires: // live ticket
   369  		jitter = liveJitter
   370  	default: // unmined ticket
   371  		jitter = unminedJitter
   372  	}
   373  
   374  	return prng.duration(jitter)
   375  }
   376  
   377  // task returns a function running a feePayment method.
   378  // If the method errors, the error is logged, and the payment is put
   379  // in an errored state and may require manual processing.
   380  func (fp *feePayment) task(name string, method func() error) func() {
   381  	return func() {
   382  		err := method()
   383  		fp.mu.Lock()
   384  		fp.err = err
   385  		fp.mu.Unlock()
   386  		if err != nil {
   387  			log.Errorf("ticket %v: %v: %v", &fp.ticketHash, name, err)
   388  		}
   389  	}
   390  }
   391  
   392  func (fp *feePayment) stop() {
   393  	fp.schedule("", nil)
   394  }
   395  
   396  func (fp *feePayment) receiveFeeAddress() error {
   397  	ctx := fp.ctx
   398  	w := fp.client.wallet
   399  	params := w.ChainParams()
   400  
   401  	// stop processing if ticket is expired or spent
   402  	if fp.removedExpiredOrSpent() {
   403  		// nothing scheduled
   404  		return errStopped
   405  	}
   406  
   407  	// Fetch ticket and its parent transaction (typically, a split
   408  	// transaction).
   409  	ticket, err := fp.client.tx(ctx, &fp.ticketHash)
   410  	if err != nil {
   411  		return fmt.Errorf("failed to retrieve ticket: %w", err)
   412  	}
   413  	parentHash := &ticket.TxIn[0].PreviousOutPoint.Hash
   414  	parent, err := fp.client.tx(ctx, parentHash)
   415  	if err != nil {
   416  		return fmt.Errorf("failed to retrieve parent %v of ticket: %w",
   417  			parentHash, err)
   418  	}
   419  
   420  	ticketHex, err := marshalTx(ticket)
   421  	if err != nil {
   422  		return err
   423  	}
   424  	parentHex, err := marshalTx(parent)
   425  	if err != nil {
   426  		return err
   427  	}
   428  
   429  	req := types.FeeAddressRequest{
   430  		Timestamp:  time.Now().Unix(),
   431  		TicketHash: fp.ticketHash.String(),
   432  		TicketHex:  ticketHex,
   433  		ParentHex:  parentHex,
   434  	}
   435  
   436  	resp, err := fp.client.FeeAddress(ctx, req, fp.commitmentAddr)
   437  	if err != nil {
   438  		return err
   439  	}
   440  
   441  	feeAmount := dcrutil.Amount(resp.FeeAmount)
   442  	feeAddr, err := stdaddr.DecodeAddress(resp.FeeAddress, params)
   443  	if err != nil {
   444  		return fmt.Errorf("server fee address invalid: %w", err)
   445  	}
   446  
   447  	log.Infof("VSP requires fee %v", feeAmount)
   448  	if feeAmount > fp.policy.MaxFee {
   449  		return fmt.Errorf("server fee amount too high: %v > %v",
   450  			feeAmount, fp.policy.MaxFee)
   451  	}
   452  
   453  	// XXX validate server timestamp?
   454  
   455  	fp.mu.Lock()
   456  	fp.fee = feeAmount
   457  	fp.feeAddr = feeAddr
   458  	fp.mu.Unlock()
   459  
   460  	return nil
   461  }
   462  
   463  // makeFeeTx adds outputs to tx to pay a VSP fee, optionally adding inputs as
   464  // well to fund the transaction if no input value is already provided in the
   465  // transaction.
   466  //
   467  // If tx is nil, fp.feeTx may be assigned or modified, but the pointer will not
   468  // be dereferenced.
   469  func (fp *feePayment) makeFeeTx(tx *wire.MsgTx) error {
   470  	ctx := fp.ctx
   471  	w := fp.client.wallet
   472  
   473  	fp.mu.Lock()
   474  	fee := fp.fee
   475  	fpFeeTx := fp.feeTx
   476  	feeAddr := fp.feeAddr
   477  	fp.mu.Unlock()
   478  
   479  	// The rest of this function will operate on the tx pointer, with fp.feeTx
   480  	// assigned to the result on success.
   481  	// Update tx to use the partially created fpFeeTx if any has been started.
   482  	// The transaction pointed to by the caller will be dereferenced and modified
   483  	// when non-nil.
   484  	if fpFeeTx != nil {
   485  		if tx != nil {
   486  			*tx = *fpFeeTx
   487  		} else {
   488  			tx = fpFeeTx
   489  		}
   490  	}
   491  	// Fee transaction with outputs is already finished.
   492  	if fpFeeTx != nil && len(fpFeeTx.TxOut) != 0 {
   493  		return nil
   494  	}
   495  	// When both transactions are nil, create a new empty transaction.
   496  	if tx == nil {
   497  		tx = wire.NewMsgTx()
   498  	}
   499  
   500  	// XXX fp.fee == -1?
   501  	if fee == 0 {
   502  		err := fp.receiveFeeAddress()
   503  		if err != nil {
   504  			return err
   505  		}
   506  		fp.mu.Lock()
   507  		fee = fp.fee
   508  		feeAddr = fp.feeAddr
   509  		fp.mu.Unlock()
   510  	}
   511  
   512  	// Reserve new outputs to pay the fee if outputs have not already been
   513  	// reserved.  This will be the case for fee payments that were begun on
   514  	// already purchased tickets, where the caller did not ensure that fee
   515  	// outputs would already be reserved.
   516  	if len(tx.TxIn) == 0 {
   517  		const minconf = 1
   518  		inputs, err := w.ReserveOutputsForAmount(ctx, fp.policy.FeeAcct, fee, minconf)
   519  		if err != nil {
   520  			return fmt.Errorf("unable to reserve enough output value to "+
   521  				"pay VSP fee for ticket %v: %w", fp.ticketHash, err)
   522  		}
   523  		for _, in := range inputs {
   524  			tx.AddTxIn(wire.NewTxIn(&in.OutPoint, in.PrevOut.Value, nil))
   525  		}
   526  		// The transaction will be added to the wallet in an unpublished
   527  		// state, so there is no need to leave the outputs locked.
   528  		defer func() {
   529  			for _, in := range inputs {
   530  				w.UnlockOutpoint(&in.OutPoint.Hash, in.OutPoint.Index)
   531  			}
   532  		}()
   533  	}
   534  
   535  	var input int64
   536  	for _, in := range tx.TxIn {
   537  		input += in.ValueIn
   538  	}
   539  	if input < int64(fee) {
   540  		err := fmt.Errorf("not enough input value to pay fee: %v < %v",
   541  			dcrutil.Amount(input), fee)
   542  		return err
   543  	}
   544  
   545  	vers, feeScript := feeAddr.PaymentScript()
   546  
   547  	addr, err := w.NewChangeAddress(ctx, fp.policy.ChangeAcct)
   548  	if err != nil {
   549  		log.Warnf("failed to get new change address: %v", err)
   550  		return err
   551  	}
   552  	var changeOut *wire.TxOut
   553  	switch addr := addr.(type) {
   554  	case wallet.Address:
   555  		vers, script := addr.PaymentScript()
   556  		changeOut = &wire.TxOut{PkScript: script, Version: vers}
   557  	default:
   558  		return fmt.Errorf("failed to convert '%T' to wallet.Address", addr)
   559  	}
   560  
   561  	tx.TxOut = append(tx.TxOut[:0], &wire.TxOut{
   562  		Value:    int64(fee),
   563  		Version:  vers,
   564  		PkScript: feeScript,
   565  	})
   566  	feeRate := w.RelayFee()
   567  	scriptSizes := make([]int, len(tx.TxIn))
   568  	for i := range scriptSizes {
   569  		scriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize
   570  	}
   571  	est := txsizes.EstimateSerializeSize(scriptSizes, tx.TxOut, txsizes.P2PKHPkScriptSize)
   572  	change := input
   573  	change -= tx.TxOut[0].Value
   574  	change -= int64(txrules.FeeForSerializeSize(feeRate, est))
   575  	if !txrules.IsDustAmount(dcrutil.Amount(change), txsizes.P2PKHPkScriptSize, feeRate) {
   576  		changeOut.Value = change
   577  		tx.TxOut = append(tx.TxOut, changeOut)
   578  		// randomize position
   579  		if prng.coinflip() {
   580  			tx.TxOut[0], tx.TxOut[1] = tx.TxOut[1], tx.TxOut[0]
   581  		}
   582  	}
   583  
   584  	feeHash := tx.TxHash()
   585  
   586  	// sign
   587  	sigErrs, err := w.SignTransaction(ctx, tx, txscript.SigHashAll, nil, nil, nil)
   588  	if err != nil || len(sigErrs) > 0 {
   589  		log.Errorf("failed to sign transaction: %v", err)
   590  		sigErrStr := ""
   591  		for _, sigErr := range sigErrs {
   592  			log.Errorf("\t%v", sigErr)
   593  			sigErrStr = fmt.Sprintf("\t%v", sigErr) + " "
   594  		}
   595  		if err != nil {
   596  			return err
   597  		}
   598  		return fmt.Errorf(sigErrStr)
   599  	}
   600  
   601  	err = w.SetPublished(ctx, &feeHash, false)
   602  	if err != nil {
   603  		return err
   604  	}
   605  	err = w.AddTransaction(ctx, tx, nil)
   606  	if err != nil {
   607  		return err
   608  	}
   609  	err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey)
   610  	if err != nil {
   611  		return err
   612  	}
   613  
   614  	fp.mu.Lock()
   615  	fp.feeTx = tx
   616  	fp.feeHash = feeHash
   617  	fp.mu.Unlock()
   618  
   619  	// nothing scheduled
   620  	return nil
   621  }
   622  
   623  func (c *Client) status(ctx context.Context, ticketHash *chainhash.Hash) (*types.TicketStatusResponse, error) {
   624  	w := c.wallet
   625  	params := w.ChainParams()
   626  
   627  	ticketTx, err := c.tx(ctx, ticketHash)
   628  	if err != nil {
   629  		return nil, fmt.Errorf("failed to retrieve ticket %v: %w", ticketHash, err)
   630  	}
   631  	if len(ticketTx.TxOut) != 3 {
   632  		return nil, fmt.Errorf("ticket %v has multiple commitments: %w", ticketHash, errNotSolo)
   633  	}
   634  
   635  	if !stake.IsSStx(ticketTx) {
   636  		return nil, fmt.Errorf("%v is not a ticket", ticketHash)
   637  	}
   638  	commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, params)
   639  	if err != nil {
   640  		return nil, fmt.Errorf("failed to extract commitment address from %v: %w",
   641  			ticketHash, err)
   642  	}
   643  
   644  	req := types.TicketStatusRequest{
   645  		TicketHash: ticketHash.String(),
   646  	}
   647  
   648  	resp, err := c.Client.TicketStatus(ctx, req, commitmentAddr)
   649  	if err != nil {
   650  		return nil, err
   651  	}
   652  
   653  	// XXX validate server timestamp?
   654  
   655  	return resp, nil
   656  }
   657  
   658  func (c *Client) setVoteChoices(ctx context.Context, ticketHash *chainhash.Hash,
   659  	choices map[string]string, tspendPolicy map[string]string, treasuryPolicy map[string]string) error {
   660  	w := c.wallet
   661  	params := w.ChainParams()
   662  
   663  	ticketTx, err := c.tx(ctx, ticketHash)
   664  	if err != nil {
   665  		return fmt.Errorf("failed to retrieve ticket %v: %w", ticketHash, err)
   666  	}
   667  
   668  	if !stake.IsSStx(ticketTx) {
   669  		return fmt.Errorf("%v is not a ticket", ticketHash)
   670  	}
   671  	if len(ticketTx.TxOut) != 3 {
   672  		return fmt.Errorf("ticket %v has multiple commitments: %w", ticketHash, errNotSolo)
   673  	}
   674  
   675  	commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, params)
   676  	if err != nil {
   677  		return fmt.Errorf("failed to extract commitment address from %v: %w",
   678  			ticketHash, err)
   679  	}
   680  
   681  	req := types.SetVoteChoicesRequest{
   682  		Timestamp:      time.Now().Unix(),
   683  		TicketHash:     ticketHash.String(),
   684  		VoteChoices:    choices,
   685  		TSpendPolicy:   tspendPolicy,
   686  		TreasuryPolicy: treasuryPolicy,
   687  	}
   688  
   689  	_, err = c.Client.SetVoteChoices(ctx, req, commitmentAddr)
   690  	if err != nil {
   691  		return err
   692  	}
   693  
   694  	// XXX validate server timestamp?
   695  
   696  	return nil
   697  }
   698  
   699  func (fp *feePayment) reconcilePayment() error {
   700  	ctx := fp.ctx
   701  	w := fp.client.wallet
   702  
   703  	// stop processing if ticket is expired or spent
   704  	// XXX if ticket is no longer saved by wallet (because the tx expired,
   705  	// or was double spent, etc) remove the fee payment.
   706  	if fp.removedExpiredOrSpent() {
   707  		// nothing scheduled
   708  		return errStopped
   709  	}
   710  
   711  	// A fee amount and address must have been created by this point.
   712  	// Ensure that the fee transaction can be created, otherwise reschedule
   713  	// this method until it is.  There is no need to check the wallet for a
   714  	// fee transaction matching a known hash; this is performed when
   715  	// creating the feePayment.
   716  	fp.mu.Lock()
   717  	feeTx := fp.feeTx
   718  	fp.mu.Unlock()
   719  	if feeTx == nil || len(feeTx.TxOut) == 0 {
   720  		err := fp.makeFeeTx(nil)
   721  		if err != nil {
   722  			var apiErr types.ErrorResponse
   723  			if errors.As(err, &apiErr) && apiErr.Code == types.ErrTicketCannotVote {
   724  				fp.remove("ticket cannot vote")
   725  			}
   726  			return err
   727  		}
   728  	}
   729  
   730  	// A fee address has been obtained, and the fee transaction has been
   731  	// created, but it is unknown if the VSP has received the fee and will
   732  	// vote using the ticket.
   733  	//
   734  	// If the fee is mined, then check the status of the ticket and payment
   735  	// with the VSP, to ensure that it has marked the fee payment as paid.
   736  	//
   737  	// If the fee is not mined, an API call with the VSP is used so it may
   738  	// receive and publish the transaction.  A follow up on the ticket
   739  	// status is scheduled for some time in the future.
   740  
   741  	err := fp.submitPayment()
   742  	fp.mu.Lock()
   743  	feeHash := fp.feeHash
   744  	fp.mu.Unlock()
   745  	var apiErr types.ErrorResponse
   746  	if errors.As(err, &apiErr) {
   747  		switch apiErr.Code {
   748  		case types.ErrFeeAlreadyReceived:
   749  			err = w.SetPublished(ctx, &feeHash, true)
   750  			if err != nil {
   751  				return err
   752  			}
   753  			err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey)
   754  			if err != nil {
   755  				return err
   756  			}
   757  			err = nil
   758  		case types.ErrInvalidFeeTx, types.ErrCannotBroadcastFee:
   759  			err := w.UpdateVspTicketFeeToErrored(ctx, &fp.ticketHash, fp.client.URL, fp.client.PubKey)
   760  			if err != nil {
   761  				return err
   762  			}
   763  			// Attempt to create a new fee transaction
   764  			fp.mu.Lock()
   765  			fp.feeHash = chainhash.Hash{}
   766  			fp.feeTx = nil
   767  			fp.mu.Unlock()
   768  			// err not nilled, so reconcile payment is rescheduled.
   769  		}
   770  	}
   771  	if err != nil {
   772  		// Nothing left to try except trying again.
   773  		fp.schedule("reconcile payment", fp.reconcilePayment)
   774  		return err
   775  	}
   776  
   777  	err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey)
   778  	if err != nil {
   779  		return err
   780  	}
   781  
   782  	// confirmPayment will remove the fee payment processing when the fee
   783  	// has reached sufficient confirmations, and reschedule itself if the
   784  	// fee is not confirmed yet.  If the fee tx is ever removed from the
   785  	// wallet, this will schedule another reconcile.
   786  	return fp.confirmPayment()
   787  
   788  	/*
   789  		// XXX? for each input, c.Wallet.UnlockOutpoint(&outpoint.Hash, outpoint.Index)
   790  		// xxx, or let the published tx replace the unpublished one, and unlock
   791  		// outpoints as it is processed.
   792  
   793  	*/
   794  }
   795  
   796  func (fp *feePayment) submitPayment() (err error) {
   797  	ctx := fp.ctx
   798  	w := fp.client.wallet
   799  
   800  	// stop processing if ticket is expired or spent
   801  	if fp.removedExpiredOrSpent() {
   802  		// nothing scheduled
   803  		return errStopped
   804  	}
   805  
   806  	// submitting a payment requires the fee tx to already be created.
   807  	fp.mu.Lock()
   808  	feeTx := fp.feeTx
   809  	votingKey := fp.votingKey
   810  	fp.mu.Unlock()
   811  	if feeTx == nil {
   812  		feeTx = new(wire.MsgTx)
   813  	}
   814  	if len(feeTx.TxOut) == 0 {
   815  		err := fp.makeFeeTx(feeTx)
   816  		if err != nil {
   817  			return err
   818  		}
   819  	}
   820  	if votingKey == "" {
   821  		votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr)
   822  		if err != nil {
   823  			return err
   824  		}
   825  		fp.mu.Lock()
   826  		fp.votingKey = votingKey
   827  		fp.mu.Unlock()
   828  	}
   829  
   830  	// Retrieve voting preferences
   831  	voteChoices := make(map[string]string)
   832  	agendaChoices, _, err := w.AgendaChoices(ctx, &fp.ticketHash)
   833  	if err != nil {
   834  		return err
   835  	}
   836  	for _, agendaChoice := range agendaChoices {
   837  		voteChoices[agendaChoice.AgendaID] = agendaChoice.ChoiceID
   838  	}
   839  
   840  	feeTxHex, err := marshalTx(feeTx)
   841  	if err != nil {
   842  		return err
   843  	}
   844  
   845  	req := types.PayFeeRequest{
   846  		Timestamp:      time.Now().Unix(),
   847  		TicketHash:     fp.ticketHash.String(),
   848  		FeeTx:          feeTxHex,
   849  		VotingKey:      votingKey,
   850  		VoteChoices:    voteChoices,
   851  		TSpendPolicy:   w.TSpendPolicyForTicket(&fp.ticketHash),
   852  		TreasuryPolicy: w.TreasuryKeyPolicyForTicket(&fp.ticketHash),
   853  	}
   854  
   855  	_, err = fp.client.PayFee(ctx, req, fp.commitmentAddr)
   856  	if err != nil {
   857  		var apiErr types.ErrorResponse
   858  		if errors.As(err, &apiErr) && apiErr.Code == types.ErrFeeExpired {
   859  			// Fee has been expired, so abandon current feetx, set fp.feeTx
   860  			// to nil and retry submit payment to make a new fee tx.
   861  			feeHash := feeTx.TxHash()
   862  			err := w.AbandonTransaction(ctx, &feeHash)
   863  			if err != nil {
   864  				log.Errorf("error abandoning expired fee tx %v", err)
   865  			}
   866  			fp.mu.Lock()
   867  			fp.feeTx = nil
   868  			fp.mu.Unlock()
   869  		}
   870  		return fmt.Errorf("payfee: %w", err)
   871  	}
   872  
   873  	// TODO - validate server timestamp?
   874  
   875  	log.Infof("successfully processed %v", fp.ticketHash)
   876  	return nil
   877  }
   878  
   879  func (fp *feePayment) confirmPayment() (err error) {
   880  	ctx := fp.ctx
   881  	w := fp.client.wallet
   882  
   883  	// stop processing if ticket is expired or spent
   884  	if fp.removedExpiredOrSpent() {
   885  		// nothing scheduled
   886  		return errStopped
   887  	}
   888  
   889  	defer func() {
   890  		if err != nil && !errors.Is(err, errStopped) {
   891  			fp.schedule("reconcile payment", fp.reconcilePayment)
   892  		}
   893  	}()
   894  
   895  	status, err := fp.client.status(ctx, &fp.ticketHash)
   896  	if err != nil {
   897  		log.Warnf("Rescheduling status check for %v: %v", &fp.ticketHash, err)
   898  		fp.schedule("confirm payment", fp.confirmPayment)
   899  		return nil
   900  	}
   901  
   902  	switch status.FeeTxStatus {
   903  	case "received":
   904  		// VSP has received the fee tx but has not yet broadcast it.
   905  		// VSP will only broadcast the tx when ticket has 6+ confirmations.
   906  		fp.schedule("confirm payment", fp.confirmPayment)
   907  		return nil
   908  	case "broadcast":
   909  		log.Infof("VSP has successfully sent the fee tx for %v", &fp.ticketHash)
   910  		// Broadcasted, but not confirmed.
   911  		fp.schedule("confirm payment", fp.confirmPayment)
   912  		return nil
   913  	case "confirmed":
   914  		fp.remove("confirmed by VSP")
   915  		// nothing scheduled
   916  		fp.mu.Lock()
   917  		feeHash := fp.feeHash
   918  		fp.mu.Unlock()
   919  		err = w.UpdateVspTicketFeeToConfirmed(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey)
   920  		if err != nil {
   921  			return err
   922  		}
   923  		return nil
   924  	case "error":
   925  		log.Warnf("VSP failed to broadcast feetx for %v -- restarting payment",
   926  			&fp.ticketHash)
   927  		fp.schedule("reconcile payment", fp.reconcilePayment)
   928  		return nil
   929  	default:
   930  		// XXX put in unknown state
   931  		log.Warnf("VSP responded with %v for %v", status.FeeTxStatus,
   932  			&fp.ticketHash)
   933  	}
   934  
   935  	return nil
   936  }
   937  
   938  func marshalTx(tx *wire.MsgTx) (string, error) {
   939  	var buf bytes.Buffer
   940  	buf.Grow(tx.SerializeSize() * 2)
   941  	err := tx.Serialize(hex.NewEncoder(&buf))
   942  	return buf.String(), err
   943  }