diff --git a/go.mod b/go.mod index 524f6b5a6..7c48f978d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/crypto-power/cryptopower go 1.19 require ( - decred.org/dcrwallet/v3 v3.0.1 + decred.org/dcrwallet/v4 v4.0.0-20230809150859-a87fa843495e gioui.org v0.1.0 github.com/JohannesKaufmann/html-to-markdown v1.2.1 github.com/PuerkitoBio/goquery v1.6.1 @@ -37,6 +37,8 @@ require ( github.com/decred/dcrdata/v8 v8.0.0-20230617164141-fa4d8e1b4e8e github.com/decred/politeia v1.4.0 github.com/decred/slog v1.2.0 + github.com/decred/vspd/client/v2 v2.0.0 + github.com/decred/vspd/types/v2 v2.0.0 github.com/dgraph-io/badger v1.6.2 github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a github.com/gomarkdown/markdown v0.0.0-20220817224203-2206187d3406 @@ -100,8 +102,6 @@ require ( github.com/decred/dcrd/txscript/v3 v3.0.0 // indirect github.com/decred/dcrtime v0.0.0-20191018193024-8d8b4ef0458e // indirect github.com/decred/go-socks v1.1.0 // indirect - github.com/decred/vspd/client/v2 v2.0.0 // indirect - github.com/decred/vspd/types/v2 v2.0.0 // indirect github.com/dgraph-io/ristretto v0.0.2 // indirect github.com/dustin/go-humanize v1.0.1-0.20210705192016-249ff6c91207 // indirect github.com/fogleman/gg v1.3.0 // indirect diff --git a/go.sum b/go.sum index 81193a8e3..af5092e88 100644 --- a/go.sum +++ b/go.sum @@ -79,8 +79,8 @@ decred.org/cspp/v2 v2.1.0 h1:HeHb9+BFqrBaAPc6CsPiUpPFmC1uyBM2mJZUAbUXkRw= decred.org/cspp/v2 v2.1.0/go.mod h1:9nO3bfvCheOPIFZw5f6sRQ42CjBFB5RKSaJ9Iq6G4MA= decred.org/dcrwallet v1.7.0 h1:U/ew00YBdUlx3rJAynt2OdKDgGzBKK4O89FijBq8iVg= decred.org/dcrwallet v1.7.0/go.mod h1:hNOGyvH53gWdgFB601/ubGRzCPfPtWnEVAi9Grs90y4= -decred.org/dcrwallet/v3 v3.0.1 h1:+OLi+u/MvKc3Ubcnf19oyG/a5hJ/qp4OtezdiQZnLIs= -decred.org/dcrwallet/v3 v3.0.1/go.mod h1:a+R8BZIOKVpWVPat5VZoBWNh/cnIciwcRkPtrzfS/tw= +decred.org/dcrwallet/v4 v4.0.0-20230809150859-a87fa843495e h1:Y1f5xYRQi7qMzLYifvAOplMhgHiKxtWWzd1psXqsS6A= +decred.org/dcrwallet/v4 v4.0.0-20230809150859-a87fa843495e/go.mod h1:0+CchVf/baDYJ0tlDjtEzAFsvYcwWRm0eo1+Lf7Z1as= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= gioui.org v0.1.0 h1:fEDY5A4+epOdzjCBYSUC4BzvjWqsjfqf5D6mskbthOs= diff --git a/libwallet/assets/btc/accounts.go b/libwallet/assets/btc/accounts.go index 6e653e1eb..842e156e9 100644 --- a/libwallet/assets/btc/accounts.go +++ b/libwallet/assets/btc/accounts.go @@ -7,7 +7,7 @@ import ( "strconv" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcwallet/waddrmgr" diff --git a/libwallet/assets/btc/address.go b/libwallet/assets/btc/address.go index 9edf2c127..28b5e6372 100644 --- a/libwallet/assets/btc/address.go +++ b/libwallet/assets/btc/address.go @@ -3,7 +3,7 @@ package btc import ( "fmt" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/btcsuite/btcd/btcutil" "github.com/crypto-power/cryptopower/libwallet/utils" ) diff --git a/libwallet/assets/btc/feerate.go b/libwallet/assets/btc/feerate.go index bd41d428a..609a26f3d 100644 --- a/libwallet/assets/btc/feerate.go +++ b/libwallet/assets/btc/feerate.go @@ -7,7 +7,7 @@ import ( "strconv" "sync" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/btcsuite/btcd/btcutil" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" diff --git a/libwallet/assets/btc/rescan.go b/libwallet/assets/btc/rescan.go index 9843805fa..4d3608d02 100644 --- a/libwallet/assets/btc/rescan.go +++ b/libwallet/assets/btc/rescan.go @@ -6,7 +6,7 @@ import ( "sync/atomic" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/waddrmgr" diff --git a/libwallet/assets/btc/sync.go b/libwallet/assets/btc/sync.go index f1be049e3..23a5f5e3d 100644 --- a/libwallet/assets/btc/sync.go +++ b/libwallet/assets/btc/sync.go @@ -7,7 +7,7 @@ import ( "sync/atomic" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/btcsuite/btcwallet/chain" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" diff --git a/libwallet/assets/btc/txandblocknotifications.go b/libwallet/assets/btc/txandblocknotifications.go index 8974090ca..de0ad3826 100644 --- a/libwallet/assets/btc/txandblocknotifications.go +++ b/libwallet/assets/btc/txandblocknotifications.go @@ -4,7 +4,7 @@ import ( "encoding/json" "sync/atomic" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" ) diff --git a/libwallet/assets/btc/txauthor.go b/libwallet/assets/btc/txauthor.go index 0d3941ca6..62b021755 100644 --- a/libwallet/assets/btc/txauthor.go +++ b/libwallet/assets/btc/txauthor.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" diff --git a/libwallet/assets/btc/wallet.go b/libwallet/assets/btc/wallet.go index 90ecee53c..affa0430b 100644 --- a/libwallet/assets/btc/wallet.go +++ b/libwallet/assets/btc/wallet.go @@ -9,7 +9,7 @@ import ( "sync" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil/gcs" diff --git a/libwallet/assets/dcr/account_mixer.go b/libwallet/assets/dcr/account_mixer.go index c2d9c561a..6024ff268 100644 --- a/libwallet/assets/dcr/account_mixer.go +++ b/libwallet/assets/dcr/account_mixer.go @@ -7,9 +7,9 @@ import ( "errors" "net" - "decred.org/dcrwallet/v3/ticketbuyer" - w "decred.org/dcrwallet/v3/wallet" - "decred.org/dcrwallet/v3/wallet/udb" + "decred.org/dcrwallet/v4/ticketbuyer" + w "decred.org/dcrwallet/v4/wallet" + "decred.org/dcrwallet/v4/wallet/udb" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/internal/certs" "github.com/crypto-power/cryptopower/libwallet/utils" diff --git a/libwallet/assets/dcr/accounts.go b/libwallet/assets/dcr/accounts.go index 99d9b2c83..d631f45fc 100644 --- a/libwallet/assets/dcr/accounts.go +++ b/libwallet/assets/dcr/accounts.go @@ -6,8 +6,8 @@ import ( "fmt" "strconv" - "decred.org/dcrwallet/v3/errors" - w "decred.org/dcrwallet/v3/wallet" + "decred.org/dcrwallet/v4/errors" + w "decred.org/dcrwallet/v4/wallet" "github.com/crypto-power/cryptopower/libwallet/addresshelper" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" diff --git a/libwallet/assets/dcr/address.go b/libwallet/assets/dcr/address.go index d041cdeaf..b6f72da27 100644 --- a/libwallet/assets/dcr/address.go +++ b/libwallet/assets/dcr/address.go @@ -3,8 +3,8 @@ package dcr import ( "fmt" - "decred.org/dcrwallet/v3/errors" - w "decred.org/dcrwallet/v3/wallet" + "decred.org/dcrwallet/v4/errors" + w "decred.org/dcrwallet/v4/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/decred/dcrd/txscript/v4/stdaddr" ) diff --git a/libwallet/assets/dcr/consensus.go b/libwallet/assets/dcr/consensus.go index 04ed1e486..51b8ef370 100644 --- a/libwallet/assets/dcr/consensus.go +++ b/libwallet/assets/dcr/consensus.go @@ -6,8 +6,7 @@ import ( "sort" "strings" - "decred.org/dcrwallet/v3/errors" - w "decred.org/dcrwallet/v3/wallet" + "decred.org/dcrwallet/v4/errors" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/decred/dcrd/chaincfg/chainhash" @@ -111,22 +110,19 @@ func (asset *Asset) SetVoteChoice(agendaID, choiceID, hash, passphrase string) e return err } - currentChoice := w.AgendaChoice{ - AgendaID: agendaID, - ChoiceID: "abstain", // default to abstain as current choice if not found in wallet + choice, ok := choices[agendaID] + if ok && choice == strings.ToLower(choiceID) { + // Do not set the same choice again + return nil } - for i := range choices { - if choices[i].AgendaID == agendaID { - currentChoice.ChoiceID = choices[i].ChoiceID - break - } + if !ok { + // Default to abstain if no previous choice existed + choice = "abstain" } - newChoice := w.AgendaChoice{ - AgendaID: agendaID, - ChoiceID: strings.ToLower(choiceID), - } + currentChoice := map[string]string{agendaID: choice} + newChoice := map[string]string{agendaID: strings.ToLower(choiceID)} _, err = asset.Internal().DCR.SetAgendaChoices(ctx, ticketHash, newChoice) if err != nil { @@ -181,7 +177,7 @@ func (asset *Asset) SetVoteChoice(agendaID, choiceID, hash, passphrase string) e firstErr = err continue // try next tHash } - err = vspClient.SetVoteChoice(ctx, tHash, []w.AgendaChoice{newChoice}, nil, nil) + err = vspClient.SetVoteChoice(ctx, tHash, newChoice, nil, nil) if err != nil && firstErr == nil { firstErr = err continue // try next tHash @@ -245,9 +241,9 @@ func (asset *Asset) AllVoteAgendas(hash string, newestFirst bool) ([]*Agenda, er d := &deployments[i] votingPreference := "abstain" // assume abstain, if we have the saved pref, it'll be updated below - for c := range choices { - if choices[c].AgendaID == d.Vote.Id { - votingPreference = choices[c].ChoiceID + for agendaID, choiceID := range choices { + if agendaID == d.Vote.Id { + votingPreference = choiceID break } } diff --git a/libwallet/assets/dcr/decodetx.go b/libwallet/assets/dcr/decodetx.go index a679d07b7..0fd4ab3f3 100644 --- a/libwallet/assets/dcr/decodetx.go +++ b/libwallet/assets/dcr/decodetx.go @@ -3,7 +3,7 @@ package dcr import ( "fmt" - w "decred.org/dcrwallet/v3/wallet" + w "decred.org/dcrwallet/v4/wallet" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/txhelper" "github.com/decred/dcrd/blockchain/stake/v5" diff --git a/libwallet/assets/dcr/message.go b/libwallet/assets/dcr/message.go index f60cd11f9..438017554 100644 --- a/libwallet/assets/dcr/message.go +++ b/libwallet/assets/dcr/message.go @@ -1,8 +1,8 @@ package dcr import ( - "decred.org/dcrwallet/v3/errors" - w "decred.org/dcrwallet/v3/wallet" + "decred.org/dcrwallet/v4/errors" + w "decred.org/dcrwallet/v4/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/decred/dcrd/txscript/v4/stdaddr" ) diff --git a/libwallet/assets/dcr/rescan.go b/libwallet/assets/dcr/rescan.go index 0b99549d3..768d1c022 100644 --- a/libwallet/assets/dcr/rescan.go +++ b/libwallet/assets/dcr/rescan.go @@ -5,8 +5,8 @@ import ( "math" "time" - "decred.org/dcrwallet/v3/errors" - w "decred.org/dcrwallet/v3/wallet" + "decred.org/dcrwallet/v4/errors" + w "decred.org/dcrwallet/v4/wallet" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" ) diff --git a/libwallet/assets/dcr/sync.go b/libwallet/assets/dcr/sync.go index 8be6e7e6c..c672be79d 100644 --- a/libwallet/assets/dcr/sync.go +++ b/libwallet/assets/dcr/sync.go @@ -9,10 +9,10 @@ import ( "strings" "sync" - "decred.org/dcrwallet/v3/errors" - "decred.org/dcrwallet/v3/p2p" - "decred.org/dcrwallet/v3/spv" - w "decred.org/dcrwallet/v3/wallet" + "decred.org/dcrwallet/v4/errors" + "decred.org/dcrwallet/v4/p2p" + "decred.org/dcrwallet/v4/spv" + w "decred.org/dcrwallet/v4/wallet" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/decred/dcrd/addrmgr/v2" diff --git a/libwallet/assets/dcr/syncnotification.go b/libwallet/assets/dcr/syncnotification.go index 8825126ed..0d898403f 100644 --- a/libwallet/assets/dcr/syncnotification.go +++ b/libwallet/assets/dcr/syncnotification.go @@ -4,7 +4,7 @@ import ( "math" "time" - "decred.org/dcrwallet/v3/spv" + "decred.org/dcrwallet/v4/spv" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "golang.org/x/sync/errgroup" ) diff --git a/libwallet/assets/dcr/ticket.go b/libwallet/assets/dcr/ticket.go index 6fe988433..3a97a6bc5 100644 --- a/libwallet/assets/dcr/ticket.go +++ b/libwallet/assets/dcr/ticket.go @@ -7,14 +7,15 @@ import ( "sync" "time" - "decred.org/dcrwallet/v3/errors" - w "decred.org/dcrwallet/v3/wallet" + "decred.org/dcrwallet/v4/errors" + w "decred.org/dcrwallet/v4/wallet" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" - "github.com/crypto-power/cryptopower/libwallet/internal/vsp" "github.com/crypto-power/cryptopower/libwallet/utils" + "github.com/decred/dcrd/blockchain/stake/v5" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrutil/v4" "github.com/decred/dcrd/wire" + "github.com/decred/vspd/types/v2" ) func (asset *Asset) TotalStakingRewards() (int64, error) { @@ -127,9 +128,9 @@ func (asset *Asset) PurchaseTickets(account, numTickets int32, vspHost, passphra Count: int(numTickets), SourceAccount: uint32(account), MinConf: asset.RequiredConfirmations(), - VSPFeeProcess: vspClient.FeePercentage, + VSPFeePercent: vspClient.FeePercentage, VSPFeePaymentProcess: func(ctx context.Context, ticketHash *chainhash.Hash, feeTx *wire.MsgTx) error { - return vspClient.Process(ctx, ticketHash, feeTx, asset.GetvspPolicy(account)) + return vspClient.Process(ctx, ticketHash, feeTx) }, } @@ -152,17 +153,6 @@ func (asset *Asset) PurchaseTickets(account, numTickets int32, vspHost, passphra return ticketsResponse.TicketHashes, err } -// GetvspPolicy creates the VSP policy using the account number provided. -// Uses the user-specified instructions for processing fee payments -// on a ticket, rather than some default policy. -func (asset *Asset) GetvspPolicy(account int32) vsp.Policy { - return vsp.Policy{ - MaxFee: 0.2e8, - FeeAcct: uint32(account), - ChangeAcct: uint32(account), - } -} - // VSPTicketInfo returns vsp-related info for a given ticket. Returns an error // if the ticket is not yet assigned to a VSP. func (asset *Asset) VSPTicketInfo(hash string) (*VSPTicketInfo, error) { @@ -204,7 +194,22 @@ func (asset *Asset) VSPTicketInfo(hash string) (*VSPTicketInfo, error) { ticketInfo.Client = vspClient - vspTicketStatus, err := vspClient.GetTicketStatus(ctx, ticketHash) + txs, _, err := asset.Internal().DCR.GetTransactionsByHashes(ctx, []*chainhash.Hash{ticketHash}) + if err != nil { + return nil, err + } + + commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(txs[0].TxOut[1].PkScript, asset.chainParams) + if err != nil { + return nil, fmt.Errorf("failed to extract commitment address from %v: %w", + ticketHash, err) + } + + req := types.TicketStatusRequest{ + TicketHash: ticketHash.String(), + } + + vspTicketStatus, err := vspClient.TicketStatus(ctx, req, commitmentAddr) if err != nil { log.Warnf("unable to get vsp ticket: %s Error: %v", hash, err) return ticketInfo, nil @@ -452,19 +457,14 @@ func (asset *Asset) buyTicket(ctx context.Context, passphrase string, sdiff dcru // Count is 1 to prevent combining multiple split outputs in one tx, // which can be used to link the tickets eventually purchased with the // split outputs. - vspPolicy := vsp.Policy{ - MaxFee: 0.2e8, - FeeAcct: uint32(cfg.PurchaseAccount), - ChangeAcct: uint32(cfg.PurchaseAccount), - } request := &w.PurchaseTicketsRequest{ Count: 1, SourceAccount: uint32(cfg.PurchaseAccount), Expiry: expiry, MinConf: asset.RequiredConfirmations(), - VSPFeeProcess: cfg.VspClient.FeePercentage, + VSPFeePercent: cfg.VspClient.FeePercentage, VSPFeePaymentProcess: func(ctx context.Context, ticketHash *chainhash.Hash, feeTx *wire.MsgTx) error { - return cfg.VspClient.Process(ctx, ticketHash, feeTx, vspPolicy) + return cfg.VspClient.Process(ctx, ticketHash, feeTx) }, } // Mixed split buying through CoinShuffle++, if configured. diff --git a/libwallet/assets/dcr/treasury.go b/libwallet/assets/dcr/treasury.go index 2d68ac5f0..35481d8d3 100644 --- a/libwallet/assets/dcr/treasury.go +++ b/libwallet/assets/dcr/treasury.go @@ -4,7 +4,7 @@ import ( "encoding/hex" "fmt" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/decred/dcrd/blockchain/stake/v5" diff --git a/libwallet/assets/dcr/txandblocknotifications.go b/libwallet/assets/dcr/txandblocknotifications.go index f5e046033..1b9b7246b 100644 --- a/libwallet/assets/dcr/txandblocknotifications.go +++ b/libwallet/assets/dcr/txandblocknotifications.go @@ -3,7 +3,7 @@ package dcr import ( "encoding/json" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" ) diff --git a/libwallet/assets/dcr/txauthor.go b/libwallet/assets/dcr/txauthor.go index ffe2566ba..5f7c545cc 100644 --- a/libwallet/assets/dcr/txauthor.go +++ b/libwallet/assets/dcr/txauthor.go @@ -7,11 +7,11 @@ import ( "fmt" "time" - "decred.org/dcrwallet/v3/errors" - w "decred.org/dcrwallet/v3/wallet" - "decred.org/dcrwallet/v3/wallet/txauthor" - "decred.org/dcrwallet/v3/wallet/txrules" - "decred.org/dcrwallet/v3/wallet/txsizes" + "decred.org/dcrwallet/v4/errors" + w "decred.org/dcrwallet/v4/wallet" + "decred.org/dcrwallet/v4/wallet/txauthor" + "decred.org/dcrwallet/v4/wallet/txrules" + "decred.org/dcrwallet/v4/wallet/txsizes" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/txhelper" "github.com/crypto-power/cryptopower/libwallet/utils" diff --git a/libwallet/assets/dcr/txindex.go b/libwallet/assets/dcr/txindex.go index ad8e98bfc..5bfb16222 100644 --- a/libwallet/assets/dcr/txindex.go +++ b/libwallet/assets/dcr/txindex.go @@ -1,7 +1,7 @@ package dcr import ( - w "decred.org/dcrwallet/v3/wallet" + w "decred.org/dcrwallet/v4/wallet" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/decred/dcrd/chaincfg/chainhash" diff --git a/libwallet/assets/dcr/txparser.go b/libwallet/assets/dcr/txparser.go index 0c0541cb0..fe8ec6137 100644 --- a/libwallet/assets/dcr/txparser.go +++ b/libwallet/assets/dcr/txparser.go @@ -3,7 +3,7 @@ package dcr import ( "fmt" - w "decred.org/dcrwallet/v3/wallet" + w "decred.org/dcrwallet/v4/wallet" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/decred/dcrd/chaincfg/chainhash" ) diff --git a/libwallet/assets/dcr/types.go b/libwallet/assets/dcr/types.go index 8519c43d3..0a051a6ec 100644 --- a/libwallet/assets/dcr/types.go +++ b/libwallet/assets/dcr/types.go @@ -5,7 +5,7 @@ import ( "fmt" "net" - "decred.org/dcrwallet/v3/wallet/udb" + "decred.org/dcrwallet/v4/wallet/udb" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/internal/vsp" "github.com/decred/dcrd/chaincfg/v3" diff --git a/libwallet/assets/dcr/vsp.go b/libwallet/assets/dcr/vsp.go index ca3a3c7a7..ab225f126 100644 --- a/libwallet/assets/dcr/vsp.go +++ b/libwallet/assets/dcr/vsp.go @@ -9,7 +9,7 @@ import ( "net/http" "strings" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/internal/vsp" "github.com/crypto-power/cryptopower/libwallet/utils" diff --git a/libwallet/assets/dcr/wallet.go b/libwallet/assets/dcr/wallet.go index e4991b56b..0a1687a3b 100644 --- a/libwallet/assets/dcr/wallet.go +++ b/libwallet/assets/dcr/wallet.go @@ -5,8 +5,8 @@ import ( "path/filepath" "sync" - dcrW "decred.org/dcrwallet/v3/wallet" - "decred.org/dcrwallet/v3/wallet/txrules" + dcrW "decred.org/dcrwallet/v4/wallet" + "decred.org/dcrwallet/v4/wallet/txrules" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/internal/loader" "github.com/crypto-power/cryptopower/libwallet/internal/loader/dcr" @@ -60,7 +60,6 @@ func initWalletLoader(chainParams *chaincfg.Params, rootdir, walletDbDriver stri stakeOptions := &dcr.StakeOptions{ VotingEnabled: false, - AddressReuse: false, VotingAddress: nil, } @@ -82,6 +81,7 @@ func initWalletLoader(chainParams *chaincfg.Params, rootdir, walletDbDriver stri ManualTickets: cfg.ManualTickets, AccountGapLimit: cfg.AccountGapLimit, MixSplitLimit: cfg.MixSplitLimit, + WatchLast: 20, // Limit number of watched addresses to 20. } walletLoader := dcr.NewLoader(loaderCfg) diff --git a/libwallet/assets/ltc/accounts.go b/libwallet/assets/ltc/accounts.go index 2f0f55e59..fbfac536d 100644 --- a/libwallet/assets/ltc/accounts.go +++ b/libwallet/assets/ltc/accounts.go @@ -7,7 +7,7 @@ import ( "strconv" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/ltcsuite/ltcd/chaincfg" diff --git a/libwallet/assets/ltc/address.go b/libwallet/assets/ltc/address.go index b18003f7d..f2263fa28 100644 --- a/libwallet/assets/ltc/address.go +++ b/libwallet/assets/ltc/address.go @@ -3,7 +3,7 @@ package ltc import ( "fmt" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/ltcsuite/ltcd/ltcutil" ) diff --git a/libwallet/assets/ltc/feerate.go b/libwallet/assets/ltc/feerate.go index 48964965a..359283350 100644 --- a/libwallet/assets/ltc/feerate.go +++ b/libwallet/assets/ltc/feerate.go @@ -7,7 +7,7 @@ import ( "strconv" "sync" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/ltcsuite/ltcd/ltcutil" diff --git a/libwallet/assets/ltc/rescan.go b/libwallet/assets/ltc/rescan.go index 6b978c742..8c10773a8 100644 --- a/libwallet/assets/ltc/rescan.go +++ b/libwallet/assets/ltc/rescan.go @@ -6,7 +6,7 @@ import ( "sync/atomic" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/ltcsuite/ltcd/ltcutil" diff --git a/libwallet/assets/ltc/sync.go b/libwallet/assets/ltc/sync.go index 31050c349..d36127185 100644 --- a/libwallet/assets/ltc/sync.go +++ b/libwallet/assets/ltc/sync.go @@ -7,7 +7,7 @@ import ( "sync/atomic" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/ltcsuite/ltcd/chaincfg" diff --git a/libwallet/assets/ltc/txandblocknotifications.go b/libwallet/assets/ltc/txandblocknotifications.go index f88397e6e..43a2c9fc7 100644 --- a/libwallet/assets/ltc/txandblocknotifications.go +++ b/libwallet/assets/ltc/txandblocknotifications.go @@ -4,7 +4,7 @@ import ( "encoding/json" "sync/atomic" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" ) diff --git a/libwallet/assets/ltc/txauthor.go b/libwallet/assets/ltc/txauthor.go index 52242dd29..ff8d0da05 100644 --- a/libwallet/assets/ltc/txauthor.go +++ b/libwallet/assets/ltc/txauthor.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/txhelper" "github.com/crypto-power/cryptopower/libwallet/utils" diff --git a/libwallet/assets/ltc/utils.go b/libwallet/assets/ltc/utils.go index 28bce33d0..ea51faaa8 100644 --- a/libwallet/assets/ltc/utils.go +++ b/libwallet/assets/ltc/utils.go @@ -3,7 +3,7 @@ package ltc import ( "encoding/binary" - "decred.org/dcrwallet/v3/walletseed" + "decred.org/dcrwallet/v4/walletseed" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/ltcsuite/ltcd/chaincfg" diff --git a/libwallet/assets/ltc/wallet.go b/libwallet/assets/ltc/wallet.go index 84a269645..f34ddedf5 100644 --- a/libwallet/assets/ltc/wallet.go +++ b/libwallet/assets/ltc/wallet.go @@ -10,7 +10,7 @@ import ( "sync" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/internal/loader" "github.com/crypto-power/cryptopower/libwallet/internal/loader/ltc" diff --git a/libwallet/assets/wallet/wallet_shared.go b/libwallet/assets/wallet/wallet_shared.go index 42691fbb3..6bc55451b 100644 --- a/libwallet/assets/wallet/wallet_shared.go +++ b/libwallet/assets/wallet/wallet_shared.go @@ -9,8 +9,8 @@ import ( "sync" "time" - "decred.org/dcrwallet/v3/errors" - w "decred.org/dcrwallet/v3/wallet" + "decred.org/dcrwallet/v4/errors" + w "decred.org/dcrwallet/v4/wallet" "github.com/asdine/storm" "github.com/crypto-power/cryptopower/libwallet/assets/wallet/walletdata" "github.com/crypto-power/cryptopower/libwallet/internal/loader" diff --git a/libwallet/assets/wallet/wallet_utils.go b/libwallet/assets/wallet/wallet_utils.go index 5071cbfc7..3d510c514 100644 --- a/libwallet/assets/wallet/wallet_utils.go +++ b/libwallet/assets/wallet/wallet_utils.go @@ -6,8 +6,8 @@ import ( "os" "strconv" - "decred.org/dcrwallet/v3/errors" - "decred.org/dcrwallet/v3/walletseed" + "decred.org/dcrwallet/v4/errors" + "decred.org/dcrwallet/v4/walletseed" "github.com/asdine/storm" btchdkeychain "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/crypto-power/cryptopower/libwallet/utils" diff --git a/libwallet/assets/wallet/walletdata/save.go b/libwallet/assets/wallet/walletdata/save.go index 37de75622..3f673c869 100644 --- a/libwallet/assets/wallet/walletdata/save.go +++ b/libwallet/assets/wallet/walletdata/save.go @@ -4,7 +4,7 @@ import ( "fmt" "reflect" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/asdine/storm" ) diff --git a/libwallet/assets_config.go b/libwallet/assets_config.go index 1133bb59e..5271f4f93 100644 --- a/libwallet/assets_config.go +++ b/libwallet/assets_config.go @@ -3,7 +3,7 @@ package libwallet import ( "fmt" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/asdine/storm" "github.com/crypto-power/cryptopower/libwallet/utils" diff --git a/libwallet/assets_manager.go b/libwallet/assets_manager.go index b4645c82d..876ffccad 100644 --- a/libwallet/assets_manager.go +++ b/libwallet/assets_manager.go @@ -7,7 +7,7 @@ import ( "path/filepath" "strconv" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/asdine/storm" "github.com/asdine/storm/q" "github.com/crypto-power/cryptopower/libwallet/ext" diff --git a/libwallet/badgerdb/bucket.go b/libwallet/badgerdb/bucket.go index bfa71af52..19767c634 100644 --- a/libwallet/badgerdb/bucket.go +++ b/libwallet/badgerdb/bucket.go @@ -3,7 +3,7 @@ package badgerdb import ( "bytes" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/dgraph-io/badger" ) diff --git a/libwallet/badgerdb/db.go b/libwallet/badgerdb/db.go index 1bf604884..41e1dc442 100644 --- a/libwallet/badgerdb/db.go +++ b/libwallet/badgerdb/db.go @@ -10,8 +10,8 @@ import ( "io" "os" - "decred.org/dcrwallet/v3/errors" - "decred.org/dcrwallet/v3/wallet/walletdb" + "decred.org/dcrwallet/v4/errors" + "decred.org/dcrwallet/v4/wallet/walletdb" "github.com/dgraph-io/badger" "github.com/dgraph-io/badger/options" ) diff --git a/libwallet/badgerdb/driver.go b/libwallet/badgerdb/driver.go index 5a2ed3eed..b693e008c 100644 --- a/libwallet/badgerdb/driver.go +++ b/libwallet/badgerdb/driver.go @@ -8,8 +8,8 @@ package badgerdb import ( "fmt" - "decred.org/dcrwallet/v3/errors" - "decred.org/dcrwallet/v3/wallet/walletdb" + "decred.org/dcrwallet/v4/errors" + "decred.org/dcrwallet/v4/wallet/walletdb" ) const ( diff --git a/libwallet/btc.go b/libwallet/btc.go index 567cce705..fc821423d 100644 --- a/libwallet/btc.go +++ b/libwallet/btc.go @@ -3,7 +3,7 @@ package libwallet import ( "fmt" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcwallet/waddrmgr" diff --git a/libwallet/dcr.go b/libwallet/dcr.go index d646463f8..b988360de 100644 --- a/libwallet/dcr.go +++ b/libwallet/dcr.go @@ -3,8 +3,8 @@ package libwallet import ( "context" - "decred.org/dcrwallet/v3/errors" - "decred.org/dcrwallet/v3/walletseed" + "decred.org/dcrwallet/v4/errors" + "decred.org/dcrwallet/v4/walletseed" "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/hdkeychain/v3" diff --git a/libwallet/instantswap.go b/libwallet/instantswap.go index b5dee2b2f..0d94a0b40 100644 --- a/libwallet/instantswap.go +++ b/libwallet/instantswap.go @@ -5,7 +5,7 @@ import ( "math" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" api "github.com/crypto-power/instantswap/instantswap" "github.com/crypto-power/cryptopower/libwallet/assets/btc" diff --git a/libwallet/instantswap/instantswap.go b/libwallet/instantswap/instantswap.go index fccf2fc80..074e61b4e 100644 --- a/libwallet/instantswap/instantswap.go +++ b/libwallet/instantswap/instantswap.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/asdine/storm" "github.com/asdine/storm/q" "github.com/crypto-power/instantswap/instantswap" diff --git a/libwallet/instantswap/sync.go b/libwallet/instantswap/sync.go index b0958335d..52c43198f 100644 --- a/libwallet/instantswap/sync.go +++ b/libwallet/instantswap/sync.go @@ -4,7 +4,7 @@ import ( "context" "time" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/asdine/storm" "github.com/crypto-power/instantswap/instantswap" ) diff --git a/libwallet/internal/loader/config.go b/libwallet/internal/loader/config.go index 84f0263f8..07548a8af 100644 --- a/libwallet/internal/loader/config.go +++ b/libwallet/internal/loader/config.go @@ -5,8 +5,8 @@ import ( "os" "path/filepath" - "decred.org/dcrwallet/v3/errors" - dcrW "decred.org/dcrwallet/v3/wallet" + "decred.org/dcrwallet/v4/errors" + dcrW "decred.org/dcrwallet/v4/wallet" btcW "github.com/btcsuite/btcwallet/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" ltcW "github.com/ltcsuite/ltcwallet/wallet" diff --git a/libwallet/internal/loader/dcr/loader.go b/libwallet/internal/loader/dcr/loader.go index ead8ac3f1..072b996b3 100644 --- a/libwallet/internal/loader/dcr/loader.go +++ b/libwallet/internal/loader/dcr/loader.go @@ -10,15 +10,15 @@ import ( "path/filepath" "sync" - "decred.org/dcrwallet/v3/errors" - "decred.org/dcrwallet/v3/wallet" + "decred.org/dcrwallet/v4/errors" + "decred.org/dcrwallet/v4/wallet" "github.com/crypto-power/cryptopower/libwallet/internal/loader" "github.com/crypto-power/cryptopower/libwallet/utils" "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/dcrutil/v4" "github.com/decred/dcrd/txscript/v4/stdaddr" - _ "decred.org/dcrwallet/v3/wallet/drivers/bdb" // driver loaded during init + _ "decred.org/dcrwallet/v4/wallet/drivers/bdb" // driver loaded during init ) const walletDbName = "wallet.db" @@ -48,6 +48,7 @@ type dcrLoader struct { manualTickets bool relayFee dcrutil.Amount mixSplitLimit int + watchLast uint32 mu sync.RWMutex } @@ -55,7 +56,6 @@ type dcrLoader struct { // StakeOptions contains the various options necessary for stake mining. type StakeOptions struct { VotingEnabled bool - AddressReuse bool VotingAddress stdaddr.StakeAddress PoolAddress stdaddr.StakeAddress PoolFees float64 @@ -77,6 +77,7 @@ type LoaderConf struct { ManualTickets bool AccountGapLimit int MixSplitLimit int + WatchLast uint32 } // NewLoader constructs a DCR Loader. @@ -91,6 +92,7 @@ func NewLoader(cfg *LoaderConf) loader.AssetLoader { manualTickets: cfg.ManualTickets, relayFee: cfg.RelayFee, mixSplitLimit: cfg.MixSplitLimit, + watchLast: cfg.WatchLast, Loader: loader.NewLoader(cfg.DBDirPath), } @@ -157,7 +159,7 @@ func (l *dcrLoader) CreateWatchingOnlyWallet(ctx context.Context, params *loader DB: db, PubPassphrase: params.PubPassphrase, VotingEnabled: so.VotingEnabled, - AddressReuse: so.AddressReuse, + WatchLast: l.watchLast, VotingAddress: so.VotingAddress, PoolAddress: so.PoolAddress, PoolFees: so.PoolFees, @@ -215,7 +217,7 @@ func (l *dcrLoader) CreateNewWallet(ctx context.Context, params *loader.CreateWa DB: db, PubPassphrase: params.PubPassphrase, VotingEnabled: so.VotingEnabled, - AddressReuse: so.AddressReuse, + WatchLast: l.watchLast, VotingAddress: so.VotingAddress, PoolAddress: so.PoolAddress, PoolFees: so.PoolFees, @@ -280,7 +282,7 @@ func (l *dcrLoader) OpenExistingWallet(ctx context.Context, walletID string, pub DB: db, PubPassphrase: pubPassphrase, VotingEnabled: so.VotingEnabled, - AddressReuse: so.AddressReuse, + WatchLast: l.watchLast, VotingAddress: so.VotingAddress, PoolAddress: so.PoolAddress, PoolFees: so.PoolFees, diff --git a/libwallet/internal/politeia/errors.go b/libwallet/internal/politeia/errors.go index 1d18f7ae6..1bb1e6267 100644 --- a/libwallet/internal/politeia/errors.go +++ b/libwallet/internal/politeia/errors.go @@ -1,7 +1,7 @@ package politeia import ( - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/asdine/storm" ) diff --git a/libwallet/internal/politeia/politeia.go b/libwallet/internal/politeia/politeia.go index 238ecb092..1ba7bd6fd 100644 --- a/libwallet/internal/politeia/politeia.go +++ b/libwallet/internal/politeia/politeia.go @@ -6,7 +6,7 @@ import ( "fmt" "sync" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/asdine/storm" "github.com/asdine/storm/q" ) diff --git a/libwallet/internal/politeia/politeia_sync.go b/libwallet/internal/politeia/politeia_sync.go index 7c5e52344..3ff95bbb6 100644 --- a/libwallet/internal/politeia/politeia_sync.go +++ b/libwallet/internal/politeia/politeia_sync.go @@ -11,8 +11,8 @@ import ( "strconv" "time" - "decred.org/dcrwallet/v3/wallet" - "decred.org/dcrwallet/v3/wallet/udb" + "decred.org/dcrwallet/v4/wallet" + "decred.org/dcrwallet/v4/wallet/udb" "github.com/asdine/storm" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/txscript/v4/stdaddr" diff --git a/libwallet/internal/vsp/client.go b/libwallet/internal/vsp/client.go deleted file mode 100644 index 94a5c888b..000000000 --- a/libwallet/internal/vsp/client.go +++ /dev/null @@ -1,105 +0,0 @@ -package vsp - -// import ( -// "context" -// "crypto/ed25519" -// "encoding/base64" -// "encoding/json" -// "fmt" -// "net/http" - -// "github.com/crypto-power/cryptopower/libwallet/utils" -// "github.com/decred/dcrd/txscript/v4/stdaddr" -// ) - -// type client struct { -// http.Client -// url string -// pub []byte -// sign func(context.Context, string, stdaddr.Address) ([]byte, error) -// } - -// type signer interface { -// SignMessage(ctx context.Context, message string, address stdaddr.Address) ([]byte, error) -// } - -// func newClient(url string, pub []byte, s signer) *client { -// return &client{url: url, pub: pub, sign: s.SignMessage} -// } - -// type BadRequestError struct { -// HTTPStatus int `json:"-"` -// Code int `json:"code"` -// Message string `json:"message"` -// } - -// func (e *BadRequestError) Error() string { return e.Message } - -// func (c *client) post(ctx context.Context, path string, addr stdaddr.Address, response interface{}, body []byte) error { -// return c.do(ctx, http.MethodPost, path, addr, response, body) -// } - -// func (c *client) get(ctx context.Context, path string, resp interface{}) error { -// return c.do(ctx, http.MethodGet, path, nil, resp, nil) -// } - -// func (c *client) do(ctx context.Context, method, path string, addr stdaddr.Address, response interface{}, body []byte) error { -// var err error -// var sig []byte -// reqConf := &utils.ReqConfig{ -// Method: method, -// HTTPURL: c.url + path, -// IsRetByte: true, -// Headers: make(http.Header), -// } - -// if method == http.MethodPost { -// sig, err = c.sign(ctx, string(body), addr) -// if err != nil { -// return fmt.Errorf("sign request: %w", err) -// } -// reqConf.Payload = body -// } - -// // Add cookies. -// if sig != nil { -// reqConf.Headers.Add("VSP-Client-Signature", base64.StdEncoding.EncodeToString(sig)) -// } - -// respBytes := []byte{} -// reply, err := utils.HTTPRequest(reqConf, &respBytes) -// if err != nil && reply == nil { -// // Status code errors are handled below. -// return err -// } - -// status := reply.StatusCode -// is200 := status == 200 -// is4xx := status >= 400 && status <= 499 -// if !(is200 || is4xx) { -// return err -// } - -// if err = json.Unmarshal(respBytes, response); err != nil { -// return fmt.Errorf("could not pack response data: %w", err) -// } - -// sigBase64 := reply.Header.Get("VSP-Server-Signature") -// sig, err = base64.StdEncoding.DecodeString(sigBase64) -// if err != nil { -// return fmt.Errorf("cannot authenticate server: %w", err) -// } - -// if !ed25519.Verify(c.pub, respBytes, sig) { -// return fmt.Errorf("cannot authenticate server: invalid signature") -// } - -// var apiError *BadRequestError -// if is4xx { -// apiError = new(BadRequestError) -// apiError.HTTPStatus = status -// return apiError -// } - -// return nil -// } diff --git a/libwallet/internal/vsp/feepayment.go b/libwallet/internal/vsp/feepayment.go index 50edb0fad..9b07afc72 100644 --- a/libwallet/internal/vsp/feepayment.go +++ b/libwallet/internal/vsp/feepayment.go @@ -1,1018 +1,838 @@ package vsp -// import ( -// "bytes" -// "context" -// cryptorand "crypto/rand" -// "encoding/hex" -// "encoding/json" -// "fmt" -// "sync" -// "time" - -// "decred.org/dcrwallet/v3/errors" -// "decred.org/dcrwallet/v3/wallet" -// "decred.org/dcrwallet/v3/wallet/txrules" -// "decred.org/dcrwallet/v3/wallet/txsizes" -// "github.com/crypto-power/cryptopower/libwallet/internal/uniformprng" -// "github.com/decred/dcrd/blockchain/stake/v5" -// "github.com/decred/dcrd/chaincfg/chainhash" -// "github.com/decred/dcrd/chaincfg/v3" -// "github.com/decred/dcrd/dcrutil/v4" -// "github.com/decred/dcrd/txscript/v4" -// "github.com/decred/dcrd/txscript/v4/stdaddr" -// "github.com/decred/dcrd/txscript/v4/stdscript" -// "github.com/decred/dcrd/wire" -// ) - -// var prng lockedRand - -// type lockedRand struct { -// mu sync.Mutex -// rand *uniformprng.Source -// } - -// func (r *lockedRand) int63n(n int64) int64 { -// r.mu.Lock() -// defer r.mu.Unlock() -// return r.rand.Int63n(n) -// } - -// // duration returns a random time.Duration in [0,d) with uniform distribution. -// func (r *lockedRand) duration(d time.Duration) time.Duration { -// return time.Duration(r.int63n(int64(d))) -// } - -// func (r *lockedRand) coinflip() bool { -// r.mu.Lock() -// defer r.mu.Unlock() -// return r.rand.Uint32n(2) == 0 -// } - -// func init() { -// source, err := uniformprng.RandSource(cryptorand.Reader) -// if err != nil { -// panic(err) -// } -// prng = lockedRand{ -// rand: source, -// } -// } - -// var ( -// errStopped = errors.New("fee processing stopped") -// errNotSolo = errors.New("not a solo ticket") -// ) - -// // A random amount of delay (between zero and these jitter constants) is added -// // before performing some background action with the VSP. The delay is reduced -// // when a ticket is currently live, as it may be called to vote any time. -// const ( -// immatureJitter = time.Hour -// liveJitter = 5 * time.Minute -// unminedJitter = 2 * time.Minute -// ) - -// type feePayment struct { -// client *Client -// ctx context.Context - -// // Set at feepayment creation and never changes -// ticketHash chainhash.Hash -// commitmentAddr stdaddr.StakeAddress -// votingAddr stdaddr.StakeAddress -// policy Policy - -// // Requires locking for all access outside of Client.feePayment -// mu sync.Mutex -// votingKey string -// ticketLive int32 -// ticketExpires int32 -// fee dcrutil.Amount -// feeAddr stdaddr.Address -// feeHash chainhash.Hash -// feeTx *wire.MsgTx -// state state -// err error - -// timerMu sync.Mutex -// timer *time.Timer -// } - -// type state uint32 - -// const ( -// _ state = iota -// unprocessed -// feePublished -// _ // ... -// ticketSpent -// ) - -// func parseTicket(ticket *wire.MsgTx, params *chaincfg.Params) ( -// votingAddr, commitmentAddr stdaddr.StakeAddress, err error, -// ) { -// fail := func(err error) (_, _ stdaddr.StakeAddress, _ error) { -// return nil, nil, err -// } -// if !stake.IsSStx(ticket) { -// return fail(fmt.Errorf("%v is not a ticket", ticket)) -// } -// _, addrs := stdscript.ExtractAddrs(ticket.TxOut[0].Version, ticket.TxOut[0].PkScript, params) -// if len(addrs) != 1 { -// return fail(fmt.Errorf("cannot parse voting addr")) -// } -// switch addr := addrs[0].(type) { -// case stdaddr.StakeAddress: -// votingAddr = addr -// default: -// return fail(fmt.Errorf("address cannot be used for voting rights: %v", err)) -// } -// commitmentAddr, err = stake.AddrFromSStxPkScrCommitment(ticket.TxOut[1].PkScript, params) -// if err != nil { -// return fail(fmt.Errorf("cannot parse commitment address: %w", err)) -// } -// return -// } - -// func (fp *feePayment) ticketSpent() bool { -// ctx := fp.ctx -// ticketOut := wire.OutPoint{Hash: fp.ticketHash, Index: 0, Tree: 1} -// _, _, err := fp.client.Wallet.Spender(ctx, &ticketOut) -// return err == nil -// } - -// func (fp *feePayment) ticketExpired() bool { -// ctx := fp.ctx -// w := fp.client.Wallet -// _, tipHeight := w.MainChainTip(ctx) - -// fp.mu.Lock() -// expires := fp.ticketExpires -// fp.mu.Unlock() - -// return expires > 0 && tipHeight >= expires -// } - -// func (fp *feePayment) removedExpiredOrSpent() bool { -// var reason string -// switch { -// case fp.ticketExpired(): -// reason = "expired" -// case fp.ticketSpent(): -// reason = "spent" -// } -// if reason != "" { -// fp.remove(reason) -// // nothing scheduled -// return true -// } -// return false -// } - -// func (fp *feePayment) remove(reason string) { -// fp.stop() -// log.Infof("ticket %v is %s; removing from VSP client", &fp.ticketHash, reason) -// fp.client.mu.Lock() -// delete(fp.client.jobs, fp.ticketHash) -// fp.client.mu.Unlock() -// } - -// // feePayment returns an existing managed fee payment, or creates and begins -// // processing a fee payment for a ticket. -// func (c *Client) feePayment(ctx context.Context, ticketHash *chainhash.Hash, paidConfirmed bool) (fp *feePayment) { -// c.mu.Lock() -// fp = c.jobs[*ticketHash] -// c.mu.Unlock() -// if fp != nil { -// return fp -// } - -// defer func() { -// if fp == nil { -// return -// } -// var schedule bool -// c.mu.Lock() -// fp2 := c.jobs[*ticketHash] -// if fp2 != nil { -// fp.stop() -// fp = fp2 -// } else { -// c.jobs[*ticketHash] = fp -// schedule = true -// } -// c.mu.Unlock() -// if schedule { -// fp.schedule("reconcile payment", fp.reconcilePayment) -// } -// }() - -// w := c.wallet - -// fp = &feePayment{ -// client: c, -// ctx: context.Background(), -// ticketHash: *ticketHash, -// policy: c.policy, -// params: c.params, -// } - -// // No VSP interaction is required for spent tickets. -// if fp.ticketSpent() { -// fp.state = TicketSpent -// return fp -// } - -// ticket, err := c.tx(ctx, ticketHash) -// if err != nil { -// fp.client.log.Warnf("no ticket found for %v", ticketHash) -// return nil -// } - -// fp.votingAddr, fp.commitmentAddr, err = parseTicket(ticket, c.params) -// if err != nil { -// fp.client.log.Errorf("%v is not a ticket: %v", ticketHash, err) -// return nil -// } -// // Try to access the voting key. -// fp.votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr) -// if err != nil { -// fp.client.log.Errorf("no voting key for ticket %v: %v", ticketHash, err) -// return nil -// } -// feeHash, err := w.VSPFeeHashForTicket(ctx, ticketHash) -// if err != nil { -// // caller must schedule next method, as paying the fee may -// // require using provided transaction inputs. -// return fp -// } - -// fee, err := c.tx(ctx, &feeHash) -// if err != nil { -// // A fee hash is recorded for this ticket, but was not found in -// // the wallet. This should not happen and may require manual -// // intervention. -// // -// // XXX should check ticketinfo and see if fee is not paid. if -// // possible, update it with a new fee. -// fp.err = fmt.Errorf("fee transaction not found in wallet: %w", err) -// return fp -// } - -// fp.feeTx = fee -// fp.feeHash = feeHash - -// // If database has been updated to paid or confirmed status, we can forgo -// // this step. -// if !paidConfirmed { -// err = w.UpdateVspTicketFeeToStarted(ctx, ticketHash, &feeHash, c.Client.URL, c.Client.PubKey) -// if err != nil { -// return fp -// } - -// fp.state = Unprocessed // XXX fee created, but perhaps not submitted with vsp. -// fp.fee = -1 // XXX fee amount (not needed anymore?) -// } -// return fp -// } - -// func (c *Client) tx(ctx context.Context, hash *chainhash.Hash) (*wire.MsgTx, error) { -// txs, _, err := c.Wallet.GetTransactionsByHashes(ctx, []*chainhash.Hash{hash}) -// if err != nil { -// return nil, err -// } -// return txs[0], nil -// } - -// // Schedule a method to be executed. -// // Any currently-scheduled method is replaced. -// func (fp *feePayment) schedule(name string, method func() error) { -// var delay time.Duration -// if method != nil { -// delay = fp.next() -// } - -// fp.timerMu.Lock() -// defer fp.timerMu.Unlock() -// if fp.timer != nil { -// fp.timer.Stop() -// fp.timer = nil -// } -// if method != nil { -// log.Debugf("scheduling %q for ticket %s in %v", name, &fp.ticketHash, delay) -// fp.timer = time.AfterFunc(delay, fp.task(name, method)) -// } -// } - -// func (fp *feePayment) next() time.Duration { -// w := fp.client.Wallet -// params := w.ChainParams() -// _, tipHeight := w.MainChainTip(fp.ctx) - -// fp.mu.Lock() -// ticketLive := fp.ticketLive -// ticketExpires := fp.ticketExpires -// fp.mu.Unlock() - -// var jitter time.Duration -// switch { -// case tipHeight < ticketLive: // immature, mined ticket -// blocksUntilLive := ticketExpires - tipHeight -// jitter = params.TargetTimePerBlock * time.Duration(blocksUntilLive) -// if jitter > immatureJitter { -// jitter = immatureJitter -// } -// case tipHeight < ticketExpires: // live ticket -// jitter = liveJitter -// default: // unmined ticket -// jitter = unminedJitter -// } - -// return prng.duration(jitter) -// } - -// // task returns a function running a feePayment method. -// // If the method errors, the error is logged, and the payment is put -// // in an errored state and may require manual processing. -// func (fp *feePayment) task(name string, method func() error) func() { -// return func() { -// err := method() -// fp.mu.Lock() -// fp.err = err -// fp.mu.Unlock() -// if err != nil { -// log.Errorf("ticket %v: %v: %v", &fp.ticketHash, name, err) -// } -// } -// } - -// func (fp *feePayment) stop() { -// fp.schedule("", nil) -// } - -// func (fp *feePayment) receiveFeeAddress() error { -// ctx := fp.ctx -// w := fp.client.Wallet -// params := w.ChainParams() - -// // stop processing if ticket is expired or spent -// if fp.removedExpiredOrSpent() { -// // nothing scheduled -// return errStopped -// } - -// // Fetch ticket and its parent transaction (typically, a split -// // transaction). -// ticket, err := fp.client.tx(ctx, &fp.ticketHash) -// if err != nil { -// return fmt.Errorf("failed to retrieve ticket: %w", err) -// } -// parentHash := &ticket.TxIn[0].PreviousOutPoint.Hash -// parent, err := fp.client.tx(ctx, parentHash) -// if err != nil { -// return fmt.Errorf("failed to retrieve parent %v of ticket: %w", -// parentHash, err) -// } - -// var response struct { -// Timestamp int64 `json:"timestamp"` -// FeeAddress string `json:"feeaddress"` -// FeeAmount int64 `json:"feeamount"` -// Request []byte `json:"request"` -// } -// requestBody, err := json.Marshal(&struct { -// Timestamp int64 `json:"timestamp"` -// TicketHash string `json:"tickethash"` -// TicketHex json.Marshaler `json:"tickethex"` -// ParentHex json.Marshaler `json:"parenthex"` -// }{ -// Timestamp: time.Now().Unix(), -// TicketHash: fp.ticketHash.String(), -// TicketHex: txMarshaler(ticket), -// ParentHex: txMarshaler(parent), -// }) -// if err != nil { -// return err -// } -// err = fp.client.post(ctx, "/api/v3/feeaddress", fp.commitmentAddr, &response, -// json.RawMessage(requestBody)) -// if err != nil { -// return err -// } - -// // verify initial request matches server -// if !bytes.Equal(requestBody, response.Request) { -// return fmt.Errorf("server response has differing request: %#v != %#v", -// requestBody, response.Request) -// } - -// feeAmount := dcrutil.Amount(response.FeeAmount) -// feeAddr, err := stdaddr.DecodeAddress(response.FeeAddress, params) -// if err != nil { -// return fmt.Errorf("server fee address invalid: %w", err) -// } - -// log.Infof("VSP requires fee %v", feeAmount) -// if feeAmount > fp.policy.MaxFee { -// return fmt.Errorf("server fee amount too high: %v > %v", -// feeAmount, fp.policy.MaxFee) -// } - -// // XXX validate server timestamp? - -// fp.mu.Lock() -// fp.fee = feeAmount -// fp.feeAddr = feeAddr -// fp.mu.Unlock() - -// return nil -// } - -// // makeFeeTx adds outputs to tx to pay a VSP fee, optionally adding inputs as -// // well to fund the transaction if no input value is already provided in the -// // transaction. -// // -// // If tx is nil, fp.feeTx may be assigned or modified, but the pointer will not -// // be dereferenced. -// func (fp *feePayment) makeFeeTx(tx *wire.MsgTx) error { -// ctx := fp.ctx -// w := fp.client.Wallet - -// fp.mu.Lock() -// fee := fp.fee -// fpFeeTx := fp.feeTx -// feeAddr := fp.feeAddr -// fp.mu.Unlock() - -// // The rest of this function will operate on the tx pointer, with fp.feeTx -// // assigned to the result on success. -// // Update tx to use the partially created fpFeeTx if any has been started. -// // The transaction pointed to by the caller will be dereferenced and modified -// // when non-nil. -// if fpFeeTx != nil { -// if tx != nil { -// *tx = *fpFeeTx -// } else { -// tx = fpFeeTx -// } -// } -// // Fee transaction with outputs is already finished. -// if fpFeeTx != nil && len(fpFeeTx.TxOut) != 0 { -// return nil -// } -// // When both transactions are nil, create a new empty transaction. -// if tx == nil { -// tx = wire.NewMsgTx() -// } - -// // XXX fp.fee == -1? -// if fee == 0 { -// err := fp.receiveFeeAddress() -// if err != nil { -// return err -// } -// fp.mu.Lock() -// fee = fp.fee -// feeAddr = fp.feeAddr -// fp.mu.Unlock() -// } - -// // Reserve new outputs to pay the fee if outputs have not already been -// // reserved. This will the the case for fee payments that were begun on -// // already purchased tickets, where the caller did not ensure that fee -// // outputs would already be reserved. -// if len(tx.TxIn) == 0 { -// const minconf = 1 -// inputs, err := w.ReserveOutputsForAmount(ctx, fp.policy.FeeAcct, fee, minconf) -// if err != nil { -// return fmt.Errorf("unable to reserve enough output value to "+ -// "pay VSP fee for ticket %v: %w", fp.ticketHash, err) -// } -// for _, in := range inputs { -// tx.AddTxIn(wire.NewTxIn(&in.OutPoint, in.PrevOut.Value, nil)) -// } -// // The transaction will be added to the wallet in an unpublished -// // state, so there is no need to leave the outputs locked. -// defer func() { -// for _, in := range inputs { -// w.UnlockOutpoint(&in.OutPoint.Hash, in.OutPoint.Index) -// } -// }() -// } - -// var input int64 -// for _, in := range tx.TxIn { -// input += in.ValueIn -// } -// if input < int64(fee) { -// err := fmt.Errorf("not enough input value to pay fee: %v < %v", -// dcrutil.Amount(input), fee) -// return err -// } - -// vers, feeScript := feeAddr.PaymentScript() - -// addr, err := w.NewChangeAddress(ctx, fp.policy.ChangeAcct) -// if err != nil { -// log.Warnf("failed to get new change address: %v", err) -// return err -// } -// var changeOut *wire.TxOut -// switch addr := addr.(type) { -// case wallet.Address: -// vers, script := addr.PaymentScript() -// changeOut = &wire.TxOut{PkScript: script, Version: vers} -// default: -// return fmt.Errorf("failed to convert '%T' to wallet.Address", addr) -// } - -// tx.TxOut = append(tx.TxOut[:0], &wire.TxOut{ -// Value: int64(fee), -// Version: vers, -// PkScript: feeScript, -// }) -// feeRate := w.RelayFee() -// scriptSizes := make([]int, len(tx.TxIn)) -// for i := range scriptSizes { -// scriptSizes[i] = txsizes.RedeemP2PKHSigScriptSize -// } -// est := txsizes.EstimateSerializeSize(scriptSizes, tx.TxOut, txsizes.P2PKHPkScriptSize) -// change := input -// change -= tx.TxOut[0].Value -// change -= int64(txrules.FeeForSerializeSize(feeRate, est)) -// if !txrules.IsDustAmount(dcrutil.Amount(change), txsizes.P2PKHPkScriptSize, feeRate) { -// changeOut.Value = change -// tx.TxOut = append(tx.TxOut, changeOut) -// // randomize position -// if prng.coinflip() { -// tx.TxOut[0], tx.TxOut[1] = tx.TxOut[1], tx.TxOut[0] -// } -// } - -// feeHash := tx.TxHash() - -// // sign -// sigErrs, err := w.SignTransaction(ctx, tx, txscript.SigHashAll, nil, nil, nil) -// if err != nil || len(sigErrs) > 0 { -// log.Errorf("failed to sign transaction: %v", err) -// sigErrStr := "" -// for _, sigErr := range sigErrs { -// log.Errorf("\t%v", sigErr) -// sigErrStr = fmt.Sprintf("\t%v", sigErr) + " " -// } -// if err != nil { -// return err -// } -// return fmt.Errorf(sigErrStr) -// } - -// err = w.SetPublished(ctx, &feeHash, false) -// if err != nil { -// return err -// } -// err = w.AddTransaction(ctx, tx, nil) -// if err != nil { -// return err -// } - -// err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.url, fp.client.pub) -// if err != nil { -// return err -// } - -// fp.mu.Lock() -// fp.feeTx = tx -// fp.feeHash = feeHash -// fp.mu.Unlock() - -// // nothing scheduled -// return nil -// } - -// type TicketStatus struct { -// Timestamp int64 `json:"timestamp"` -// TicketConfirmed bool `json:"ticketconfirmed"` -// FeeTxStatus string `json:"feetxstatus"` -// FeeTxHash string `json:"feetxhash"` -// VoteChoices map[string]string `json:"votechoices"` -// TSpendPolicy map[string]string `json:"tspendpolicy"` -// TreasuryPolicy map[string]string `json:"treasurypolicy"` -// Request []byte `json:"request"` -// } - -// // GetTicketStatus calls the VSP's TicketStatus API for the provided ticket hash -// // and returns the VSP's response. -// func (c *Client) GetTicketStatus(ctx context.Context, ticketHash *chainhash.Hash) (*TicketStatus, error) { -// return c.status(ctx, ticketHash) -// } - -// func (c *Client) status(ctx context.Context, ticketHash *chainhash.Hash) (*TicketStatus, error) { -// w := c.Wallet -// params := w.ChainParams() - -// ticketTx, err := c.tx(ctx, ticketHash) -// if err != nil { -// return nil, fmt.Errorf("failed to retrieve ticket %v: %w", ticketHash, err) -// } -// if len(ticketTx.TxOut) != 3 { -// return nil, fmt.Errorf("ticket %v has multiple commitments: %w", ticketHash, errNotSolo) -// } - -// if !stake.IsSStx(ticketTx) { -// return nil, fmt.Errorf("%v is not a ticket", ticketHash) -// } -// commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, params) -// if err != nil { -// return nil, fmt.Errorf("failed to extract commitment address from %v: %w", -// ticketHash, err) -// } - -// var resp TicketStatus -// requestBody, err := json.Marshal(&struct { -// TicketHash string `json:"tickethash"` -// }{ -// TicketHash: ticketHash.String(), -// }) -// if err != nil { -// return nil, err -// } -// err = c.post(ctx, "/api/v3/ticketstatus", commitmentAddr, &resp, -// json.RawMessage(requestBody)) -// if err != nil { -// return nil, err -// } - -// // verify initial request matches server -// if !bytes.Equal(requestBody, resp.Request) { -// log.Warnf("server response has differing request: %#v != %#v", -// requestBody, resp.Request) -// return nil, fmt.Errorf("server response contains differing request") -// } - -// // XXX validate server timestamp? - -// return &resp, nil -// } - -// func (c *Client) setVoteChoices(ctx context.Context, ticketHash *chainhash.Hash, -// choices []wallet.AgendaChoice, tspendPolicy map[string]string, treasuryPolicy map[string]string, -// ) error { -// w := c.Wallet -// params := w.ChainParams() - -// ticketTx, err := c.tx(ctx, ticketHash) -// if err != nil { -// return fmt.Errorf("failed to retrieve ticket %v: %w", ticketHash, err) -// } - -// if !stake.IsSStx(ticketTx) { -// return fmt.Errorf("%v is not a ticket", ticketHash) -// } -// if len(ticketTx.TxOut) != 3 { -// return fmt.Errorf("ticket %v has multiple commitments: %w", ticketHash, errNotSolo) -// } - -// commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, params) -// if err != nil { -// return fmt.Errorf("failed to extract commitment address from %v: %w", -// ticketHash, err) -// } - -// agendaChoices := make(map[string]string, len(choices)) - -// // Prepare agenda choice -// for _, c := range choices { -// agendaChoices[c.AgendaID] = c.ChoiceID -// } - -// var resp TicketStatus -// requestBody, err := json.Marshal(&struct { -// Timestamp int64 `json:"timestamp"` -// TicketHash string `json:"tickethash"` -// VoteChoices map[string]string `json:"votechoices"` -// TSpendPolicy map[string]string `json:"tspendpolicy"` -// TreasuryPolicy map[string]string `json:"treasurypolicy"` -// }{ -// Timestamp: time.Now().Unix(), -// TicketHash: ticketHash.String(), -// VoteChoices: agendaChoices, -// TSpendPolicy: tspendPolicy, -// TreasuryPolicy: treasuryPolicy, -// }) -// if err != nil { -// return err -// } - -// err = c.post(ctx, "/api/v3/setvotechoices", commitmentAddr, &resp, -// json.RawMessage(requestBody)) -// if err != nil { -// return err -// } - -// // verify initial request matches server -// if !bytes.Equal(requestBody, resp.Request) { -// log.Warnf("server response has differing request: %#v != %#v", -// requestBody, resp.Request) -// return fmt.Errorf("server response contains differing request") -// } - -// // XXX validate server timestamp? - -// return nil -// } - -// func (fp *feePayment) reconcilePayment() error { -// ctx := fp.ctx -// w := fp.client.Wallet - -// // stop processing if ticket is expired or spent -// // XXX if ticket is no longer saved by wallet (because the tx expired, -// // or was double spent, etc) remove the fee payment. -// if fp.removedExpiredOrSpent() { -// // nothing scheduled -// return errStopped -// } - -// // A fee amount and address must have been created by this point. -// // Ensure that the fee transaction can be created, otherwise reschedule -// // this method until it is. There is no need to check the wallet for a -// // fee transaction matching a known hash; this is performed when -// // creating the feePayment. -// fp.mu.Lock() -// feeTx := fp.feeTx -// fp.mu.Unlock() -// if feeTx == nil || len(feeTx.TxOut) == 0 { -// err := fp.makeFeeTx(nil) -// if err != nil { -// var apiErr *BadRequestError -// if errors.As(err, &apiErr) && apiErr.Code == codeTicketCannotVote { -// fp.remove("ticket cannot vote") -// // Tickets will be automatically revoked. -// } -// return err -// } -// } - -// // A fee address has been obtained, and the fee transaction has been -// // created, but it is unknown if the VSP has received the fee and will -// // vote using the ticket. -// // -// // If the fee is mined, then check the status of the ticket and payment -// // with the VSP, to ensure that it has marked the fee payment as paid. -// // -// // If the fee is not mined, an API call with the VSP is used so it may -// // receive and publish the transaction. A follow up on the ticket -// // status is scheduled for some time in the future. - -// err := fp.submitPayment() -// fp.mu.Lock() -// feeHash := fp.feeHash -// fp.mu.Unlock() -// var apiErr *BadRequestError -// if errors.As(err, &apiErr) { -// switch apiErr.Code { -// case codeFeeAlreadyReceived: -// err = w.SetPublished(ctx, &feeHash, true) -// if err != nil { -// return err -// } -// err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.url, fp.client.pub) -// if err != nil { -// return err -// } -// err = nil -// case codeInvalidFeeTx, codeCannotBroadcastFee: -// err := w.UpdateVspTicketFeeToErrored(ctx, &fp.ticketHash, fp.client.url, fp.client.pub) -// if err != nil { -// return err -// } -// // Attempt to create a new fee transaction -// fp.mu.Lock() -// fp.feeHash = chainhash.Hash{} -// fp.feeTx = nil -// fp.mu.Unlock() -// // err not nilled, so reconcile payment is rescheduled. -// } -// } -// if err != nil { -// // Nothing left to try except trying again. -// fp.schedule("reconcile payment", fp.reconcilePayment) -// return err -// } - -// err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.url, fp.client.pub) -// if err != nil { -// return err -// } - -// // confirmPayment will remove the fee payment processing when the fee -// // has reached sufficient confirmations, and reschedule itself if the -// // fee is not confirmed yet. If the fee tx is ever removed from the -// // wallet, this will schedule another reconcile. -// return fp.confirmPayment() - -// /* -// // XXX? for each input, c.Wallet.UnlockOutpoint(&outpoint.Hash, outpoint.Index) -// // xxx, or let the published tx replace the unpublished one, and unlock -// // outpoints as it is processed. - -// */ -// } - -// func (fp *feePayment) submitPayment() (err error) { -// ctx := fp.ctx -// w := fp.client.Wallet - -// // stop processing if ticket is expired or spent -// if fp.removedExpiredOrSpent() { -// // nothing scheduled -// return errStopped -// } - -// // submitting a payment requires the fee tx to already be created. -// fp.mu.Lock() -// feeTx := fp.feeTx -// votingKey := fp.votingKey -// fp.mu.Unlock() -// if feeTx == nil { -// feeTx = new(wire.MsgTx) -// } -// if len(feeTx.TxOut) == 0 { -// err := fp.makeFeeTx(feeTx) -// if err != nil { -// return err -// } -// } -// if votingKey == "" { -// votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr) -// if err != nil { -// return err -// } -// fp.mu.Lock() -// fp.votingKey = votingKey -// fp.mu.Unlock() -// } - -// // Retrieve voting preferences -// voteChoices := make(map[string]string) -// agendaChoices, _, err := w.AgendaChoices(ctx, &fp.ticketHash) -// if err != nil { -// return err -// } -// for _, agendaChoice := range agendaChoices { -// voteChoices[agendaChoice.AgendaID] = agendaChoice.ChoiceID -// } - -// var payfeeResponse struct { -// Timestamp int64 `json:"timestamp"` -// Request []byte `json:"request"` -// } -// requestBody, err := json.Marshal(&struct { -// Timestamp int64 `json:"timestamp"` -// TicketHash string `json:"tickethash"` -// FeeTx json.Marshaler `json:"feetx"` -// VotingKey string `json:"votingkey"` -// VoteChoices map[string]string `json:"votechoices"` -// TSpendPolicy map[string]string `json:"tspendpolicy"` -// TreasuryPolicy map[string]string `json:"treasurypolicy"` -// }{ -// Timestamp: time.Now().Unix(), -// TicketHash: fp.ticketHash.String(), -// FeeTx: txMarshaler(feeTx), -// VotingKey: votingKey, -// VoteChoices: voteChoices, -// TSpendPolicy: w.TSpendPolicyForTicket(&fp.ticketHash), -// TreasuryPolicy: w.TreasuryKeyPolicyForTicket(&fp.ticketHash), -// }) -// if err != nil { -// return err -// } -// err = fp.client.post(ctx, "/api/v3/payfee", fp.commitmentAddr, -// &payfeeResponse, json.RawMessage(requestBody)) -// if err != nil { -// var apiErr *BadRequestError -// if errors.As(err, &apiErr) && apiErr.Code == codeFeeExpired { -// // Fee has been expired, so abandon current feetx, set fp.feeTx -// // to nil and retry submit payment to make a new fee tx. -// feeHash := feeTx.TxHash() -// err := w.AbandonTransaction(ctx, &feeHash) -// if err != nil { -// log.Errorf("error abandoning expired fee tx %v", err) -// } -// fp.feeTx = nil -// } -// return fmt.Errorf("payfee: %w", err) -// } - -// // Check for matching original request. -// // This is signed by the VSP, and the signature -// // has already been checked above. -// if !bytes.Equal(requestBody, payfeeResponse.Request) { -// return fmt.Errorf("server response has differing request: %#v != %#v", -// requestBody, payfeeResponse.Request) -// } -// // TODO - validate server timestamp? - -// log.Infof("successfully processed %v", fp.ticketHash) -// return nil -// } - -// func (fp *feePayment) confirmPayment() (err error) { -// ctx := fp.ctx -// w := fp.client.Wallet - -// // stop processing if ticket is expired or spent -// if fp.removedExpiredOrSpent() { -// // nothing scheduled -// return errStopped -// } - -// defer func() { -// if err != nil && !errors.Is(err, errStopped) { -// fp.schedule("reconcile payment", fp.reconcilePayment) -// } -// }() - -// status, err := fp.client.status(ctx, &fp.ticketHash) -// // Suppress log if the wallet is currently locked. -// if err != nil && !errors.Is(err, errors.Locked) { -// log.Warnf("Rescheduling status check for %v: %v", &fp.ticketHash, err) -// } -// if err != nil { -// // Stop processing if the status check cannot be performed, but -// // a significant amount of confirmations are observed on the fee -// // transaction. -// // -// // Otherwise, chedule another confirmation check, in case the -// // status API can be performed at a later time or more -// // confirmations are observed. -// fp.mu.Lock() -// feeHash := fp.feeHash -// fp.mu.Unlock() -// confs, err := w.TxConfirms(ctx, &feeHash) -// if err != nil { -// return err -// } -// if confs >= 6 { -// fp.remove("confirmed") -// err = w.UpdateVspTicketFeeToConfirmed(ctx, &fp.ticketHash, &feeHash, fp.client.url, fp.client.pub) -// if err != nil { -// return err -// } -// return nil -// } -// fp.schedule("confirm payment", fp.confirmPayment) -// return nil -// } - -// switch status.FeeTxStatus { -// case "received": -// // VSP has received the fee tx but has not yet broadcast it. -// // VSP will only broadcast the tx when ticket has 6+ confirmations. -// fp.schedule("confirm payment", fp.confirmPayment) -// return nil -// case "broadcast": -// log.Infof("VSP has successfully sent the fee tx for %v", &fp.ticketHash) -// // Broadcasted, but not confirmed. -// fp.schedule("confirm payment", fp.confirmPayment) -// return nil -// case "confirmed": -// fp.remove("confirmed by VSP") -// // nothing scheduled -// err = w.UpdateVspTicketFeeToConfirmed(ctx, &fp.ticketHash, &fp.feeHash, fp.client.url, fp.client.pub) -// if err != nil { -// return err -// } -// return nil -// case "error": -// log.Warnf("VSP failed to broadcast feetx for %v -- restarting payment", -// &fp.ticketHash) -// fp.schedule("reconcile payment", fp.reconcilePayment) -// return nil -// default: -// // XXX put in unknown state -// log.Warnf("VSP responded with %v for %v", status.FeeTxStatus, -// &fp.ticketHash) -// } - -// return nil -// } - -// type marshaler struct { -// marshaled []byte -// err error -// } - -// func (m *marshaler) MarshalJSON() ([]byte, error) { -// return m.marshaled, m.err -// } - -// func txMarshaler(tx *wire.MsgTx) json.Marshaler { -// var buf bytes.Buffer -// buf.Grow(2 + tx.SerializeSize()*2) -// buf.WriteByte('"') -// err := tx.Serialize(hex.NewEncoder(&buf)) -// buf.WriteByte('"') -// return &marshaler{buf.Bytes(), err} -// } +import ( + "bytes" + "context" + "crypto/rand" + "encoding/binary" + "encoding/hex" + "fmt" + "math/bits" + "sync" + "time" + + "decred.org/dcrwallet/v4/errors" + "github.com/decred/dcrd/blockchain/stake/v5" + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/chaincfg/v3" + "github.com/decred/dcrd/dcrutil/v4" + "github.com/decred/dcrd/txscript/v4/stdaddr" + "github.com/decred/dcrd/txscript/v4/stdscript" + "github.com/decred/dcrd/wire" + "github.com/decred/vspd/types/v2" +) + +// randInt63 returns a cryptographically random 63-bit positive integer as an +// int64. +func randInt63() int64 { + buf := make([]byte, 8) + _, err := rand.Read(buf) + if err != nil { + panic(fmt.Sprintf("unhandled crypto/rand error: %v", err)) + } + return int64(binary.LittleEndian.Uint64(buf) &^ (1 << 63)) +} + +// randInt63n returns, as an int64, a cryptographically-random 63-bit positive +// integer in [0,n) without modulo bias. +// It panics if n <= 0. +func randInt63n(n int64) int64 { + if n <= 0 { + panic("invalid argument to int63n") + } + n-- + mask := int64(^uint64(0) >> bits.LeadingZeros64(uint64(n))) + for { + v := randInt63() & mask + if v <= n { + return v + } + } +} + +// randomDuration returns a random time.Duration in [0,d) with uniform +// distribution. +func randomDuration(d time.Duration) time.Duration { + return time.Duration(randInt63n(int64(d))) +} + +var ( + errStopped = errors.New("fee processing stopped") + errNotSolo = errors.New("not a solo ticket") +) + +// A random amount of delay (between zero and these jitter constants) is added +// before performing some background action with the VSP. The delay is reduced +// when a ticket is currently live, as it may be called to vote any time. +const ( + immatureJitter = time.Hour + liveJitter = 5 * time.Minute + unminedJitter = 2 * time.Minute +) + +type feePayment struct { + client *Client + ctx context.Context + + // Set at feepayment creation and never changes + ticketHash chainhash.Hash + commitmentAddr stdaddr.StakeAddress + votingAddr stdaddr.StakeAddress + policy *Policy + + // Requires locking for all access outside of Client.feePayment + mu sync.Mutex + votingKey string + ticketLive int32 + ticketExpires int32 + fee dcrutil.Amount + feeAddr stdaddr.Address + feeHash chainhash.Hash + feeTx *wire.MsgTx + state State + err error + + timerMu sync.Mutex + timer *time.Timer + + params *chaincfg.Params +} + +type State uint32 + +const ( + _ State = iota + Unprocessed + FeePublished + _ // ... + TicketSpent +) + +func parseTicket(ticket *wire.MsgTx, params *chaincfg.Params) ( + votingAddr, commitmentAddr stdaddr.StakeAddress, err error, +) { + fail := func(err error) (_, _ stdaddr.StakeAddress, _ error) { + return nil, nil, err + } + if !stake.IsSStx(ticket) { + return fail(fmt.Errorf("%v is not a ticket", ticket)) + } + _, addrs := stdscript.ExtractAddrs(ticket.TxOut[0].Version, ticket.TxOut[0].PkScript, params) + if len(addrs) != 1 { + return fail(fmt.Errorf("cannot parse voting addr")) + } + switch addr := addrs[0].(type) { + case stdaddr.StakeAddress: + votingAddr = addr + default: + return fail(fmt.Errorf("address cannot be used for voting rights: %v", err)) + } + commitmentAddr, err = stake.AddrFromSStxPkScrCommitment(ticket.TxOut[1].PkScript, params) + if err != nil { + return fail(fmt.Errorf("cannot parse commitment address: %w", err)) + } + return +} + +// calcHeights checks if the ticket has been mined, and if so, sets the live +// height and expiry height fields. Should be called with mutex already held. +func (fp *feePayment) calcHeights() { + _, minedHeight, err := fp.client.wallet.TxBlock(fp.ctx, &fp.ticketHash) + if err != nil { + // This is not expected to ever error, as the ticket has already been + // fetched from the wallet at least one before this point is reached. + log.Errorf("Failed to query block which mines ticket: %v", err) + return + } + + if minedHeight < 2 { + return + } + + // Note the off-by-one; this is correct. Tickets become live one block after + // the params would indicate. + fp.ticketLive = minedHeight + int32(fp.params.TicketMaturity) + 1 + fp.ticketExpires = fp.ticketLive + int32(fp.params.TicketExpiry) +} + +// expiryHeight returns the height at which the ticket expires. Returns zero if +// the block is not yet mined. Should be called with mutex already held. +func (fp *feePayment) expiryHeight() int32 { + if fp.ticketExpires == 0 { + fp.calcHeights() + } + + return fp.ticketExpires +} + +// liveHeight returns the height at which the ticket becomes live. Returns zero +// if the block is not yet mined. Should be called with mutex already held. +func (fp *feePayment) liveHeight() int32 { + if fp.ticketLive == 0 { + fp.calcHeights() + } + + return fp.ticketLive +} + +func (fp *feePayment) ticketSpent() bool { + ctx := fp.ctx + ticketOut := wire.OutPoint{Hash: fp.ticketHash, Index: 0, Tree: 1} + _, _, err := fp.client.wallet.Spender(ctx, &ticketOut) + return err == nil +} + +func (fp *feePayment) ticketExpired() bool { + ctx := fp.ctx + w := fp.client.wallet + _, tipHeight := w.MainChainTip(ctx) + + fp.mu.Lock() + expires := fp.expiryHeight() + fp.mu.Unlock() + + return expires > 0 && tipHeight >= expires +} + +func (fp *feePayment) removedExpiredOrSpent() bool { + var reason string + switch { + case fp.ticketExpired(): + reason = "expired" + case fp.ticketSpent(): + reason = "spent" + } + if reason != "" { + fp.remove(reason) + // nothing scheduled + return true + } + return false +} + +func (fp *feePayment) remove(reason string) { + fp.stop() + log.Infof("ticket %v is %s; removing from VSP client", &fp.ticketHash, reason) + fp.client.mu.Lock() + delete(fp.client.jobs, fp.ticketHash) + fp.client.mu.Unlock() +} + +// feePayment returns an existing managed fee payment, or creates and begins +// processing a fee payment for a ticket. +func (c *Client) feePayment(ctx context.Context, ticketHash *chainhash.Hash, paidConfirmed bool) (fp *feePayment) { + c.mu.Lock() + fp = c.jobs[*ticketHash] + c.mu.Unlock() + if fp != nil { + return fp + } + + defer func() { + if fp == nil { + return + } + var schedule bool + c.mu.Lock() + fp2 := c.jobs[*ticketHash] + if fp2 != nil { + fp.stop() + fp = fp2 + } else { + c.jobs[*ticketHash] = fp + schedule = true + } + c.mu.Unlock() + if schedule { + fp.schedule("reconcile payment", fp.reconcilePayment) + } + }() + + w := c.wallet + + fp = &feePayment{ + client: c, + ctx: context.Background(), + ticketHash: *ticketHash, + policy: c.policy, + params: c.params, + } + + // No VSP interaction is required for spent tickets. + if fp.ticketSpent() { + fp.state = TicketSpent + return fp + } + + ticket, err := c.tx(ctx, ticketHash) + if err != nil { + log.Warnf("no ticket found for %v", ticketHash) + return nil + } + + fp.votingAddr, fp.commitmentAddr, err = parseTicket(ticket, c.params) + if err != nil { + log.Errorf("%v is not a ticket: %v", ticketHash, err) + return nil + } + // Try to access the voting key. + fp.votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr) + if err != nil { + log.Errorf("no voting key for ticket %v: %v", ticketHash, err) + return nil + } + feeHash, err := w.VSPFeeHashForTicket(ctx, ticketHash) + if err != nil { + // caller must schedule next method, as paying the fee may + // require using provided transaction inputs. + return fp + } + + fee, err := c.tx(ctx, &feeHash) + if err != nil { + // A fee hash is recorded for this ticket, but was not found in + // the wallet. This should not happen and may require manual + // intervention. + // + // XXX should check ticketinfo and see if fee is not paid. if + // possible, update it with a new fee. + fp.err = fmt.Errorf("fee transaction not found in wallet: %w", err) + return fp + } + + fp.feeTx = fee + fp.feeHash = feeHash + + // If database has been updated to paid or confirmed status, we can forgo + // this step. + if !paidConfirmed { + err = w.UpdateVspTicketFeeToStarted(ctx, ticketHash, &feeHash, c.Client.URL, c.Client.PubKey) + if err != nil { + return fp + } + + fp.state = Unprocessed // XXX fee created, but perhaps not submitted with vsp. + fp.fee = -1 // XXX fee amount (not needed anymore?) + } + return fp +} + +func (c *Client) tx(ctx context.Context, hash *chainhash.Hash) (*wire.MsgTx, error) { + txs, _, err := c.wallet.GetTransactionsByHashes(ctx, []*chainhash.Hash{hash}) + if err != nil { + return nil, err + } + return txs[0], nil +} + +// Schedule a method to be executed. +// Any currently-scheduled method is replaced. +func (fp *feePayment) schedule(name string, method func() error) { + var delay time.Duration + if method != nil { + delay = fp.next() + } + + fp.timerMu.Lock() + defer fp.timerMu.Unlock() + if fp.timer != nil { + fp.timer.Stop() + fp.timer = nil + } + if method != nil { + log.Debugf("scheduling %q for ticket %s in %v", name, &fp.ticketHash, delay) + fp.timer = time.AfterFunc(delay, fp.task(name, method)) + } +} + +func (fp *feePayment) next() time.Duration { + w := fp.client.wallet + _, tipHeight := w.MainChainTip(fp.ctx) + + fp.mu.Lock() + ticketLive := fp.liveHeight() + ticketExpires := fp.expiryHeight() + fp.mu.Unlock() + + var jitter time.Duration + switch { + case tipHeight < ticketLive: // immature, mined ticket + blocksUntilLive := ticketLive - tipHeight + jitter = fp.params.TargetTimePerBlock * time.Duration(blocksUntilLive) + if jitter > immatureJitter { + jitter = immatureJitter + } + case tipHeight < ticketExpires: // live ticket + jitter = liveJitter + default: // unmined ticket + jitter = unminedJitter + } + + return randomDuration(jitter) +} + +// task returns a function running a feePayment method. +// If the method errors, the error is logged, and the payment is put +// in an errored state and may require manual processing. +func (fp *feePayment) task(name string, method func() error) func() { + return func() { + err := method() + fp.mu.Lock() + fp.err = err + fp.mu.Unlock() + if err != nil { + log.Errorf("ticket %v: %v: %v", &fp.ticketHash, name, err) + } + } +} + +func (fp *feePayment) stop() { + fp.schedule("", nil) +} + +func (fp *feePayment) receiveFeeAddress() error { + ctx := fp.ctx + + // stop processing if ticket is expired or spent + if fp.removedExpiredOrSpent() { + // nothing scheduled + return errStopped + } + + // Fetch ticket and its parent transaction (typically, a split + // transaction). + ticket, err := fp.client.tx(ctx, &fp.ticketHash) + if err != nil { + return fmt.Errorf("failed to retrieve ticket: %w", err) + } + parentHash := &ticket.TxIn[0].PreviousOutPoint.Hash + parent, err := fp.client.tx(ctx, parentHash) + if err != nil { + return fmt.Errorf("failed to retrieve parent %v of ticket: %w", + parentHash, err) + } + + ticketHex, err := marshalTx(ticket) + if err != nil { + return err + } + parentHex, err := marshalTx(parent) + if err != nil { + return err + } + + req := types.FeeAddressRequest{ + Timestamp: time.Now().Unix(), + TicketHash: fp.ticketHash.String(), + TicketHex: ticketHex, + ParentHex: parentHex, + } + + resp, err := fp.client.FeeAddress(ctx, req, fp.commitmentAddr) + if err != nil { + return err + } + + feeAmount := dcrutil.Amount(resp.FeeAmount) + feeAddr, err := stdaddr.DecodeAddress(resp.FeeAddress, fp.params) + if err != nil { + return fmt.Errorf("server fee address invalid: %w", err) + } + + log.Infof("VSP requires fee %v", feeAmount) + if feeAmount > fp.policy.MaxFee { + return fmt.Errorf("server fee amount too high: %v > %v", + feeAmount, fp.policy.MaxFee) + } + + // XXX validate server timestamp? + + fp.mu.Lock() + fp.fee = feeAmount + fp.feeAddr = feeAddr + fp.mu.Unlock() + + return nil +} + +// makeFeeTx adds outputs to tx to pay a VSP fee, optionally adding inputs as +// well to fund the transaction if no input value is already provided in the +// transaction. +// +// If tx is nil, fp.feeTx may be assigned or modified, but the pointer will not +// be dereferenced. +func (fp *feePayment) makeFeeTx(tx *wire.MsgTx) error { + ctx := fp.ctx + w := fp.client.wallet + + fp.mu.Lock() + fee := fp.fee + fpFeeTx := fp.feeTx + feeAddr := fp.feeAddr + fp.mu.Unlock() + + // The rest of this function will operate on the tx pointer, with fp.feeTx + // assigned to the result on success. + // Update tx to use the partially created fpFeeTx if any has been started. + // The transaction pointed to by the caller will be dereferenced and modified + // when non-nil. + if fpFeeTx != nil { + if tx != nil { + *tx = *fpFeeTx + } else { + tx = fpFeeTx + } + } + // Fee transaction with outputs is already finished. + if fpFeeTx != nil && len(fpFeeTx.TxOut) != 0 { + return nil + } + // When both transactions are nil, create a new empty transaction. + if tx == nil { + tx = wire.NewMsgTx() + } + + // XXX fp.fee == -1? + if fee == 0 { + err := fp.receiveFeeAddress() + if err != nil { + return err + } + fp.mu.Lock() + fee = fp.fee + feeAddr = fp.feeAddr + fp.mu.Unlock() + } + + err := w.CreateVspPayment(ctx, tx, fee, feeAddr, fp.policy.FeeAcct, fp.policy.ChangeAcct) + if err != nil { + return fmt.Errorf("unable to create VSP fee tx for ticket %v: %w", fp.ticketHash, err) + } + + feeHash := tx.TxHash() + err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) + if err != nil { + return err + } + + fp.mu.Lock() + fp.feeTx = tx + fp.feeHash = feeHash + fp.mu.Unlock() + + // nothing scheduled + return nil +} + +func (c *Client) status(ctx context.Context, ticketHash *chainhash.Hash) (*types.TicketStatusResponse, error) { + ticketTx, err := c.tx(ctx, ticketHash) + if err != nil { + return nil, fmt.Errorf("failed to retrieve ticket %v: %w", ticketHash, err) + } + if len(ticketTx.TxOut) != 3 { + return nil, fmt.Errorf("ticket %v has multiple commitments: %w", ticketHash, errNotSolo) + } + + if !stake.IsSStx(ticketTx) { + return nil, fmt.Errorf("%v is not a ticket", ticketHash) + } + commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, c.params) + if err != nil { + return nil, fmt.Errorf("failed to extract commitment address from %v: %w", + ticketHash, err) + } + + req := types.TicketStatusRequest{ + TicketHash: ticketHash.String(), + } + + resp, err := c.Client.TicketStatus(ctx, req, commitmentAddr) + if err != nil { + return nil, err + } + + // XXX validate server timestamp? + + return resp, nil +} + +func (c *Client) setVoteChoices(ctx context.Context, ticketHash *chainhash.Hash, + choices map[string]string, tspendPolicy map[string]string, treasuryPolicy map[string]string, +) error { + ticketTx, err := c.tx(ctx, ticketHash) + if err != nil { + return fmt.Errorf("failed to retrieve ticket %v: %w", ticketHash, err) + } + + if !stake.IsSStx(ticketTx) { + return fmt.Errorf("%v is not a ticket", ticketHash) + } + if len(ticketTx.TxOut) != 3 { + return fmt.Errorf("ticket %v has multiple commitments: %w", ticketHash, errNotSolo) + } + + commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, c.params) + if err != nil { + return fmt.Errorf("failed to extract commitment address from %v: %w", + ticketHash, err) + } + + req := types.SetVoteChoicesRequest{ + Timestamp: time.Now().Unix(), + TicketHash: ticketHash.String(), + VoteChoices: choices, + TSpendPolicy: tspendPolicy, + TreasuryPolicy: treasuryPolicy, + } + + _, err = c.Client.SetVoteChoices(ctx, req, commitmentAddr) + if err != nil { + return err + } + + // XXX validate server timestamp? + + return nil +} + +func (fp *feePayment) reconcilePayment() error { + ctx := fp.ctx + w := fp.client.wallet + + // stop processing if ticket is expired or spent + // XXX if ticket is no longer saved by wallet (because the tx expired, + // or was double spent, etc) remove the fee payment. + if fp.removedExpiredOrSpent() { + // nothing scheduled + return errStopped + } + + // A fee amount and address must have been created by this point. + // Ensure that the fee transaction can be created, otherwise reschedule + // this method until it is. There is no need to check the wallet for a + // fee transaction matching a known hash; this is performed when + // creating the feePayment. + fp.mu.Lock() + feeTx := fp.feeTx + fp.mu.Unlock() + if feeTx == nil || len(feeTx.TxOut) == 0 { + err := fp.makeFeeTx(nil) + if err != nil { + var apiErr types.ErrorResponse + if errors.As(err, &apiErr) && apiErr.Code == types.ErrTicketCannotVote { + fp.remove("ticket cannot vote") + } + return err + } + } + + // A fee address has been obtained, and the fee transaction has been + // created, but it is unknown if the VSP has received the fee and will + // vote using the ticket. + // + // If the fee is mined, then check the status of the ticket and payment + // with the VSP, to ensure that it has marked the fee payment as paid. + // + // If the fee is not mined, an API call with the VSP is used so it may + // receive and publish the transaction. A follow up on the ticket + // status is scheduled for some time in the future. + + err := fp.submitPayment() + fp.mu.Lock() + feeHash := fp.feeHash + fp.mu.Unlock() + var apiErr types.ErrorResponse + if errors.As(err, &apiErr) { + switch apiErr.Code { + case types.ErrFeeAlreadyReceived: + err = w.SetPublished(ctx, &feeHash, true) + if err != nil { + return err + } + err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) + if err != nil { + return err + } + err = nil + case types.ErrInvalidFeeTx, types.ErrCannotBroadcastFee: + err := w.UpdateVspTicketFeeToErrored(ctx, &fp.ticketHash, fp.client.URL, fp.client.PubKey) + if err != nil { + return err + } + // Attempt to create a new fee transaction + fp.mu.Lock() + fp.feeHash = chainhash.Hash{} + fp.feeTx = nil + fp.mu.Unlock() + // err not nilled, so reconcile payment is rescheduled. + } + } + if err != nil { + // Nothing left to try except trying again. + fp.schedule("reconcile payment", fp.reconcilePayment) + return err + } + + err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) + if err != nil { + return err + } + + return fp.confirmPayment() + + /* + // XXX? for each input, c.Wallet.UnlockOutpoint(&outpoint.Hash, outpoint.Index) + // xxx, or let the published tx replace the unpublished one, and unlock + // outpoints as it is processed. + + */ +} + +func (fp *feePayment) submitPayment() (err error) { + ctx := fp.ctx + w := fp.client.wallet + + // stop processing if ticket is expired or spent + if fp.removedExpiredOrSpent() { + // nothing scheduled + return errStopped + } + + // submitting a payment requires the fee tx to already be created. + fp.mu.Lock() + feeTx := fp.feeTx + votingKey := fp.votingKey + fp.mu.Unlock() + if feeTx == nil { + feeTx = new(wire.MsgTx) + } + if len(feeTx.TxOut) == 0 { + err := fp.makeFeeTx(feeTx) + if err != nil { + return err + } + } + if votingKey == "" { + votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr) + if err != nil { + return err + } + fp.mu.Lock() + fp.votingKey = votingKey + fp.mu.Unlock() + } + + // Retrieve voting preferences + voteChoices, _, err := w.AgendaChoices(ctx, &fp.ticketHash) + if err != nil { + return err + } + + feeTxHex, err := marshalTx(feeTx) + if err != nil { + return err + } + + req := types.PayFeeRequest{ + Timestamp: time.Now().Unix(), + TicketHash: fp.ticketHash.String(), + FeeTx: feeTxHex, + VotingKey: votingKey, + VoteChoices: voteChoices, + TSpendPolicy: w.TSpendPolicyForTicket(&fp.ticketHash), + TreasuryPolicy: w.TreasuryKeyPolicyForTicket(&fp.ticketHash), + } + + _, err = fp.client.PayFee(ctx, req, fp.commitmentAddr) + if err != nil { + var apiErr types.ErrorResponse + if errors.As(err, &apiErr) && apiErr.Code == types.ErrFeeExpired { + // Fee has been expired, so abandon current feetx, set fp.feeTx + // to nil and retry submit payment to make a new fee tx. + feeHash := feeTx.TxHash() + err := w.AbandonTransaction(ctx, &feeHash) + if err != nil { + log.Errorf("error abandoning expired fee tx %v", err) + } + fp.mu.Lock() + fp.feeTx = nil + fp.mu.Unlock() + } + return fmt.Errorf("payfee: %w", err) + } + + // TODO - validate server timestamp? + + log.Infof("successfully processed %v", fp.ticketHash) + return nil +} + +// confirmPayment will remove the fee payment processing when the fee has +// reached sufficient confirmations, and reschedule itself if the fee is not +// confirmed yet. If the fee tx is ever removed from the wallet, this will +// schedule another reconcile. +func (fp *feePayment) confirmPayment() (err error) { + ctx := fp.ctx + w := fp.client.wallet + + // stop processing if ticket is expired or spent + if fp.removedExpiredOrSpent() { + // nothing scheduled + return errStopped + } + + defer func() { + if err != nil && !errors.Is(err, errStopped) { + fp.schedule("reconcile payment", fp.reconcilePayment) + } + }() + + status, err := fp.client.status(ctx, &fp.ticketHash) + if err != nil { + log.Warnf("Rescheduling status check for %v: %v", &fp.ticketHash, err) + fp.schedule("confirm payment", fp.confirmPayment) + return nil + } + + switch status.FeeTxStatus { + case "received": + // VSP has received the fee tx but has not yet broadcast it. + // VSP will only broadcast the tx when ticket has 6+ confirmations. + fp.schedule("confirm payment", fp.confirmPayment) + return nil + case "broadcast": + log.Infof("VSP has successfully sent the fee tx for %v", &fp.ticketHash) + // Broadcasted, but not confirmed. + fp.schedule("confirm payment", fp.confirmPayment) + return nil + case "confirmed": + fp.remove("confirmed by VSP") + // nothing scheduled + fp.mu.Lock() + feeHash := fp.feeHash + fp.mu.Unlock() + err = w.UpdateVspTicketFeeToConfirmed(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) + if err != nil { + return err + } + return nil + case "error": + log.Warnf("VSP failed to broadcast feetx for %v -- restarting payment", + &fp.ticketHash) + fp.schedule("reconcile payment", fp.reconcilePayment) + return nil + default: + // XXX put in unknown state + log.Warnf("VSP responded with unknown FeeTxStatus %q for %v", + status.FeeTxStatus, &fp.ticketHash) + } + + return nil +} + +func marshalTx(tx *wire.MsgTx) (string, error) { + var buf bytes.Buffer + buf.Grow(tx.SerializeSize() * 2) + err := tx.Serialize(hex.NewEncoder(&buf)) + return buf.String(), err +} diff --git a/libwallet/internal/vsp/feepayments.go b/libwallet/internal/vsp/feepayments.go deleted file mode 100644 index 98d868421..000000000 --- a/libwallet/internal/vsp/feepayments.go +++ /dev/null @@ -1,838 +0,0 @@ -package vsp - -import ( - "bytes" - "context" - "crypto/rand" - "encoding/binary" - "encoding/hex" - "fmt" - "math/bits" - "sync" - "time" - - "decred.org/dcrwallet/v3/errors" - "github.com/decred/dcrd/blockchain/stake/v5" - "github.com/decred/dcrd/chaincfg/chainhash" - "github.com/decred/dcrd/chaincfg/v3" - "github.com/decred/dcrd/dcrutil/v4" - "github.com/decred/dcrd/txscript/v4/stdaddr" - "github.com/decred/dcrd/txscript/v4/stdscript" - "github.com/decred/dcrd/wire" - "github.com/decred/vspd/types/v2" -) - -// randInt63 returns a cryptographically random 63-bit positive integer as an -// int64. -func randInt63() int64 { - buf := make([]byte, 8) - _, err := rand.Read(buf) - if err != nil { - panic(fmt.Sprintf("unhandled crypto/rand error: %v", err)) - } - return int64(binary.LittleEndian.Uint64(buf) &^ (1 << 63)) -} - -// randInt63n returns, as an int64, a cryptographically-random 63-bit positive -// integer in [0,n) without modulo bias. -// It panics if n <= 0. -func randInt63n(n int64) int64 { - if n <= 0 { - panic("invalid argument to int63n") - } - n-- - mask := int64(^uint64(0) >> bits.LeadingZeros64(uint64(n))) - for { - v := randInt63() & mask - if v <= n { - return v - } - } -} - -// randomDuration returns a random time.Duration in [0,d) with uniform -// distribution. -func randomDuration(d time.Duration) time.Duration { - return time.Duration(randInt63n(int64(d))) -} - -var ( - errStopped = errors.New("fee processing stopped") - errNotSolo = errors.New("not a solo ticket") -) - -// A random amount of delay (between zero and these jitter constants) is added -// before performing some background action with the VSP. The delay is reduced -// when a ticket is currently live, as it may be called to vote any time. -const ( - immatureJitter = time.Hour - liveJitter = 5 * time.Minute - unminedJitter = 2 * time.Minute -) - -type feePayment struct { - client *Client - ctx context.Context - - // Set at feepayment creation and never changes - ticketHash chainhash.Hash - commitmentAddr stdaddr.StakeAddress - votingAddr stdaddr.StakeAddress - policy *Policy - - // Requires locking for all access outside of Client.feePayment - mu sync.Mutex - votingKey string - ticketLive int32 - ticketExpires int32 - fee dcrutil.Amount - feeAddr stdaddr.Address - feeHash chainhash.Hash - feeTx *wire.MsgTx - state State - err error - - timerMu sync.Mutex - timer *time.Timer - - params *chaincfg.Params -} - -type State uint32 - -const ( - _ State = iota - Unprocessed - FeePublished - _ // ... - TicketSpent -) - -func parseTicket(ticket *wire.MsgTx, params *chaincfg.Params) ( - votingAddr, commitmentAddr stdaddr.StakeAddress, err error, -) { - fail := func(err error) (_, _ stdaddr.StakeAddress, _ error) { - return nil, nil, err - } - if !stake.IsSStx(ticket) { - return fail(fmt.Errorf("%v is not a ticket", ticket)) - } - _, addrs := stdscript.ExtractAddrs(ticket.TxOut[0].Version, ticket.TxOut[0].PkScript, params) - if len(addrs) != 1 { - return fail(fmt.Errorf("cannot parse voting addr")) - } - switch addr := addrs[0].(type) { - case stdaddr.StakeAddress: - votingAddr = addr - default: - return fail(fmt.Errorf("address cannot be used for voting rights: %v", err)) - } - commitmentAddr, err = stake.AddrFromSStxPkScrCommitment(ticket.TxOut[1].PkScript, params) - if err != nil { - return fail(fmt.Errorf("cannot parse commitment address: %w", err)) - } - return -} - -// calcHeights checks if the ticket has been mined, and if so, sets the live -// height and expiry height fields. Should be called with mutex already held. -func (fp *feePayment) calcHeights() { - _, minedHeight, err := fp.client.wallet.TxBlock(fp.ctx, &fp.ticketHash) - if err != nil { - // This is not expected to ever error, as the ticket has already been - // fetched from the wallet at least one before this point is reached. - log.Errorf("Failed to query block which mines ticket: %v", err) - return - } - - if minedHeight < 2 { - return - } - - // Note the off-by-one; this is correct. Tickets become live one block after - // the params would indicate. - fp.ticketLive = minedHeight + int32(fp.params.TicketMaturity) + 1 - fp.ticketExpires = fp.ticketLive + int32(fp.params.TicketExpiry) -} - -// expiryHeight returns the height at which the ticket expires. Returns zero if -// the block is not yet mined. Should be called with mutex already held. -func (fp *feePayment) expiryHeight() int32 { - if fp.ticketExpires == 0 { - fp.calcHeights() - } - - return fp.ticketExpires -} - -// liveHeight returns the height at which the ticket becomes live. Returns zero -// if the block is not yet mined. Should be called with mutex already held. -func (fp *feePayment) liveHeight() int32 { - if fp.ticketLive == 0 { - fp.calcHeights() - } - - return fp.ticketLive -} - -func (fp *feePayment) ticketSpent() bool { - ctx := fp.ctx - ticketOut := wire.OutPoint{Hash: fp.ticketHash, Index: 0, Tree: 1} - _, _, err := fp.client.wallet.Spender(ctx, &ticketOut) - return err == nil -} - -func (fp *feePayment) ticketExpired() bool { - ctx := fp.ctx - w := fp.client.wallet - _, tipHeight := w.MainChainTip(ctx) - - fp.mu.Lock() - expires := fp.expiryHeight() - fp.mu.Unlock() - - return expires > 0 && tipHeight >= expires -} - -func (fp *feePayment) removedExpiredOrSpent() bool { - var reason string - switch { - case fp.ticketExpired(): - reason = "expired" - case fp.ticketSpent(): - reason = "spent" - } - if reason != "" { - fp.remove(reason) - // nothing scheduled - return true - } - return false -} - -func (fp *feePayment) remove(reason string) { - fp.stop() - log.Infof("ticket %v is %s; removing from VSP client", &fp.ticketHash, reason) - fp.client.mu.Lock() - delete(fp.client.jobs, fp.ticketHash) - fp.client.mu.Unlock() -} - -// feePayment returns an existing managed fee payment, or creates and begins -// processing a fee payment for a ticket. -func (c *Client) feePayment(ctx context.Context, ticketHash *chainhash.Hash, paidConfirmed bool) (fp *feePayment) { - c.mu.Lock() - fp = c.jobs[*ticketHash] - c.mu.Unlock() - if fp != nil { - return fp - } - - defer func() { - if fp == nil { - return - } - var schedule bool - c.mu.Lock() - fp2 := c.jobs[*ticketHash] - if fp2 != nil { - fp.stop() - fp = fp2 - } else { - c.jobs[*ticketHash] = fp - schedule = true - } - c.mu.Unlock() - if schedule { - fp.schedule("reconcile payment", fp.reconcilePayment) - } - }() - - w := c.wallet - - fp = &feePayment{ - client: c, - ctx: context.Background(), - ticketHash: *ticketHash, - policy: c.policy, - params: c.params, - } - - // No VSP interaction is required for spent tickets. - if fp.ticketSpent() { - fp.state = TicketSpent - return fp - } - - ticket, err := c.tx(ctx, ticketHash) - if err != nil { - log.Warnf("no ticket found for %v", ticketHash) - return nil - } - - fp.votingAddr, fp.commitmentAddr, err = parseTicket(ticket, c.params) - if err != nil { - log.Errorf("%v is not a ticket: %v", ticketHash, err) - return nil - } - // Try to access the voting key. - fp.votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr) - if err != nil { - log.Errorf("no voting key for ticket %v: %v", ticketHash, err) - return nil - } - feeHash, err := w.VSPFeeHashForTicket(ctx, ticketHash) - if err != nil { - // caller must schedule next method, as paying the fee may - // require using provided transaction inputs. - return fp - } - - fee, err := c.tx(ctx, &feeHash) - if err != nil { - // A fee hash is recorded for this ticket, but was not found in - // the wallet. This should not happen and may require manual - // intervention. - // - // XXX should check ticketinfo and see if fee is not paid. if - // possible, update it with a new fee. - fp.err = fmt.Errorf("fee transaction not found in wallet: %w", err) - return fp - } - - fp.feeTx = fee - fp.feeHash = feeHash - - // If database has been updated to paid or confirmed status, we can forgo - // this step. - if !paidConfirmed { - err = w.UpdateVspTicketFeeToStarted(ctx, ticketHash, &feeHash, c.Client.URL, c.Client.PubKey) - if err != nil { - return fp - } - - fp.state = Unprocessed // XXX fee created, but perhaps not submitted with vsp. - fp.fee = -1 // XXX fee amount (not needed anymore?) - } - return fp -} - -func (c *Client) tx(ctx context.Context, hash *chainhash.Hash) (*wire.MsgTx, error) { - txs, _, err := c.wallet.GetTransactionsByHashes(ctx, []*chainhash.Hash{hash}) - if err != nil { - return nil, err - } - return txs[0], nil -} - -// Schedule a method to be executed. -// Any currently-scheduled method is replaced. -func (fp *feePayment) schedule(name string, method func() error) { - var delay time.Duration - if method != nil { - delay = fp.next() - } - - fp.timerMu.Lock() - defer fp.timerMu.Unlock() - if fp.timer != nil { - fp.timer.Stop() - fp.timer = nil - } - if method != nil { - log.Debugf("scheduling %q for ticket %s in %v", name, &fp.ticketHash, delay) - fp.timer = time.AfterFunc(delay, fp.task(name, method)) - } -} - -func (fp *feePayment) next() time.Duration { - w := fp.client.wallet - _, tipHeight := w.MainChainTip(fp.ctx) - - fp.mu.Lock() - ticketLive := fp.liveHeight() - ticketExpires := fp.expiryHeight() - fp.mu.Unlock() - - var jitter time.Duration - switch { - case tipHeight < ticketLive: // immature, mined ticket - blocksUntilLive := ticketLive - tipHeight - jitter = fp.params.TargetTimePerBlock * time.Duration(blocksUntilLive) - if jitter > immatureJitter { - jitter = immatureJitter - } - case tipHeight < ticketExpires: // live ticket - jitter = liveJitter - default: // unmined ticket - jitter = unminedJitter - } - - return randomDuration(jitter) -} - -// task returns a function running a feePayment method. -// If the method errors, the error is logged, and the payment is put -// in an errored state and may require manual processing. -func (fp *feePayment) task(name string, method func() error) func() { - return func() { - err := method() - fp.mu.Lock() - fp.err = err - fp.mu.Unlock() - if err != nil { - log.Errorf("ticket %v: %v: %v", &fp.ticketHash, name, err) - } - } -} - -func (fp *feePayment) stop() { - fp.schedule("", nil) -} - -func (fp *feePayment) receiveFeeAddress() error { - ctx := fp.ctx - - // stop processing if ticket is expired or spent - if fp.removedExpiredOrSpent() { - // nothing scheduled - return errStopped - } - - // Fetch ticket and its parent transaction (typically, a split - // transaction). - ticket, err := fp.client.tx(ctx, &fp.ticketHash) - if err != nil { - return fmt.Errorf("failed to retrieve ticket: %w", err) - } - parentHash := &ticket.TxIn[0].PreviousOutPoint.Hash - parent, err := fp.client.tx(ctx, parentHash) - if err != nil { - return fmt.Errorf("failed to retrieve parent %v of ticket: %w", - parentHash, err) - } - - ticketHex, err := marshalTx(ticket) - if err != nil { - return err - } - parentHex, err := marshalTx(parent) - if err != nil { - return err - } - - req := types.FeeAddressRequest{ - Timestamp: time.Now().Unix(), - TicketHash: fp.ticketHash.String(), - TicketHex: ticketHex, - ParentHex: parentHex, - } - - resp, err := fp.client.FeeAddress(ctx, req, fp.commitmentAddr) - if err != nil { - return err - } - - feeAmount := dcrutil.Amount(resp.FeeAmount) - feeAddr, err := stdaddr.DecodeAddress(resp.FeeAddress, fp.params) - if err != nil { - return fmt.Errorf("server fee address invalid: %w", err) - } - - log.Infof("VSP requires fee %v", feeAmount) - if feeAmount > fp.policy.MaxFee { - return fmt.Errorf("server fee amount too high: %v > %v", - feeAmount, fp.policy.MaxFee) - } - - // XXX validate server timestamp? - - fp.mu.Lock() - fp.fee = feeAmount - fp.feeAddr = feeAddr - fp.mu.Unlock() - - return nil -} - -// makeFeeTx adds outputs to tx to pay a VSP fee, optionally adding inputs as -// well to fund the transaction if no input value is already provided in the -// transaction. -// -// If tx is nil, fp.feeTx may be assigned or modified, but the pointer will not -// be dereferenced. -func (fp *feePayment) makeFeeTx(tx *wire.MsgTx) error { - ctx := fp.ctx - w := fp.client.wallet - - fp.mu.Lock() - fee := fp.fee - fpFeeTx := fp.feeTx - feeAddr := fp.feeAddr - fp.mu.Unlock() - - // The rest of this function will operate on the tx pointer, with fp.feeTx - // assigned to the result on success. - // Update tx to use the partially created fpFeeTx if any has been started. - // The transaction pointed to by the caller will be dereferenced and modified - // when non-nil. - if fpFeeTx != nil { - if tx != nil { - *tx = *fpFeeTx - } else { - tx = fpFeeTx - } - } - // Fee transaction with outputs is already finished. - if fpFeeTx != nil && len(fpFeeTx.TxOut) != 0 { - return nil - } - // When both transactions are nil, create a new empty transaction. - if tx == nil { - tx = wire.NewMsgTx() - } - - // XXX fp.fee == -1? - if fee == 0 { - err := fp.receiveFeeAddress() - if err != nil { - return err - } - fp.mu.Lock() - fee = fp.fee - feeAddr = fp.feeAddr - fp.mu.Unlock() - } - - err := w.CreateVspPayment(ctx, tx, fee, feeAddr, fp.policy.FeeAcct, fp.policy.ChangeAcct) - if err != nil { - return fmt.Errorf("unable to create VSP fee tx for ticket %v: %w", fp.ticketHash, err) - } - - feeHash := tx.TxHash() - err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) - if err != nil { - return err - } - - fp.mu.Lock() - fp.feeTx = tx - fp.feeHash = feeHash - fp.mu.Unlock() - - // nothing scheduled - return nil -} - -func (c *Client) status(ctx context.Context, ticketHash *chainhash.Hash) (*types.TicketStatusResponse, error) { - ticketTx, err := c.tx(ctx, ticketHash) - if err != nil { - return nil, fmt.Errorf("failed to retrieve ticket %v: %w", ticketHash, err) - } - if len(ticketTx.TxOut) != 3 { - return nil, fmt.Errorf("ticket %v has multiple commitments: %w", ticketHash, errNotSolo) - } - - if !stake.IsSStx(ticketTx) { - return nil, fmt.Errorf("%v is not a ticket", ticketHash) - } - commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, c.params) - if err != nil { - return nil, fmt.Errorf("failed to extract commitment address from %v: %w", - ticketHash, err) - } - - req := types.TicketStatusRequest{ - TicketHash: ticketHash.String(), - } - - resp, err := c.Client.TicketStatus(ctx, req, commitmentAddr) - if err != nil { - return nil, err - } - - // XXX validate server timestamp? - - return resp, nil -} - -func (c *Client) setVoteChoices(ctx context.Context, ticketHash *chainhash.Hash, - choices map[string]string, tspendPolicy map[string]string, treasuryPolicy map[string]string, -) error { - ticketTx, err := c.tx(ctx, ticketHash) - if err != nil { - return fmt.Errorf("failed to retrieve ticket %v: %w", ticketHash, err) - } - - if !stake.IsSStx(ticketTx) { - return fmt.Errorf("%v is not a ticket", ticketHash) - } - if len(ticketTx.TxOut) != 3 { - return fmt.Errorf("ticket %v has multiple commitments: %w", ticketHash, errNotSolo) - } - - commitmentAddr, err := stake.AddrFromSStxPkScrCommitment(ticketTx.TxOut[1].PkScript, c.params) - if err != nil { - return fmt.Errorf("failed to extract commitment address from %v: %w", - ticketHash, err) - } - - req := types.SetVoteChoicesRequest{ - Timestamp: time.Now().Unix(), - TicketHash: ticketHash.String(), - VoteChoices: choices, - TSpendPolicy: tspendPolicy, - TreasuryPolicy: treasuryPolicy, - } - - _, err = c.Client.SetVoteChoices(ctx, req, commitmentAddr) - if err != nil { - return err - } - - // XXX validate server timestamp? - - return nil -} - -func (fp *feePayment) reconcilePayment() error { - ctx := fp.ctx - w := fp.client.wallet - - // stop processing if ticket is expired or spent - // XXX if ticket is no longer saved by wallet (because the tx expired, - // or was double spent, etc) remove the fee payment. - if fp.removedExpiredOrSpent() { - // nothing scheduled - return errStopped - } - - // A fee amount and address must have been created by this point. - // Ensure that the fee transaction can be created, otherwise reschedule - // this method until it is. There is no need to check the wallet for a - // fee transaction matching a known hash; this is performed when - // creating the feePayment. - fp.mu.Lock() - feeTx := fp.feeTx - fp.mu.Unlock() - if feeTx == nil || len(feeTx.TxOut) == 0 { - err := fp.makeFeeTx(nil) - if err != nil { - var apiErr types.ErrorResponse - if errors.As(err, &apiErr) && apiErr.Code == types.ErrTicketCannotVote { - fp.remove("ticket cannot vote") - } - return err - } - } - - // A fee address has been obtained, and the fee transaction has been - // created, but it is unknown if the VSP has received the fee and will - // vote using the ticket. - // - // If the fee is mined, then check the status of the ticket and payment - // with the VSP, to ensure that it has marked the fee payment as paid. - // - // If the fee is not mined, an API call with the VSP is used so it may - // receive and publish the transaction. A follow up on the ticket - // status is scheduled for some time in the future. - - err := fp.submitPayment() - fp.mu.Lock() - feeHash := fp.feeHash - fp.mu.Unlock() - var apiErr types.ErrorResponse - if errors.As(err, &apiErr) { - switch apiErr.Code { - case types.ErrFeeAlreadyReceived: - err = w.SetPublished(ctx, &feeHash, true) - if err != nil { - return err - } - err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) - if err != nil { - return err - } - err = nil - case types.ErrInvalidFeeTx, types.ErrCannotBroadcastFee: - err := w.UpdateVspTicketFeeToErrored(ctx, &fp.ticketHash, fp.client.URL, fp.client.PubKey) - if err != nil { - return err - } - // Attempt to create a new fee transaction - fp.mu.Lock() - fp.feeHash = chainhash.Hash{} - fp.feeTx = nil - fp.mu.Unlock() - // err not nilled, so reconcile payment is rescheduled. - } - } - if err != nil { - // Nothing left to try except trying again. - fp.schedule("reconcile payment", fp.reconcilePayment) - return err - } - - err = w.UpdateVspTicketFeeToPaid(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) - if err != nil { - return err - } - - return fp.confirmPayment() - - /* - // XXX? for each input, c.Wallet.UnlockOutpoint(&outpoint.Hash, outpoint.Index) - // xxx, or let the published tx replace the unpublished one, and unlock - // outpoints as it is processed. - - */ -} - -func (fp *feePayment) submitPayment() (err error) { - ctx := fp.ctx - w := fp.client.wallet - - // stop processing if ticket is expired or spent - if fp.removedExpiredOrSpent() { - // nothing scheduled - return errStopped - } - - // submitting a payment requires the fee tx to already be created. - fp.mu.Lock() - feeTx := fp.feeTx - votingKey := fp.votingKey - fp.mu.Unlock() - if feeTx == nil { - feeTx = new(wire.MsgTx) - } - if len(feeTx.TxOut) == 0 { - err := fp.makeFeeTx(feeTx) - if err != nil { - return err - } - } - if votingKey == "" { - votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr) - if err != nil { - return err - } - fp.mu.Lock() - fp.votingKey = votingKey - fp.mu.Unlock() - } - - // Retrieve voting preferences - voteChoices, _, err := w.AgendaChoices(ctx, &fp.ticketHash) - if err != nil { - return err - } - - feeTxHex, err := marshalTx(feeTx) - if err != nil { - return err - } - - req := types.PayFeeRequest{ - Timestamp: time.Now().Unix(), - TicketHash: fp.ticketHash.String(), - FeeTx: feeTxHex, - VotingKey: votingKey, - VoteChoices: voteChoices, - TSpendPolicy: w.TSpendPolicyForTicket(&fp.ticketHash), - TreasuryPolicy: w.TreasuryKeyPolicyForTicket(&fp.ticketHash), - } - - _, err = fp.client.PayFee(ctx, req, fp.commitmentAddr) - if err != nil { - var apiErr types.ErrorResponse - if errors.As(err, &apiErr) && apiErr.Code == types.ErrFeeExpired { - // Fee has been expired, so abandon current feetx, set fp.feeTx - // to nil and retry submit payment to make a new fee tx. - feeHash := feeTx.TxHash() - err := w.AbandonTransaction(ctx, &feeHash) - if err != nil { - log.Errorf("error abandoning expired fee tx %v", err) - } - fp.mu.Lock() - fp.feeTx = nil - fp.mu.Unlock() - } - return fmt.Errorf("payfee: %w", err) - } - - // TODO - validate server timestamp? - - log.Infof("successfully processed %v", fp.ticketHash) - return nil -} - -// confirmPayment will remove the fee payment processing when the fee has -// reached sufficient confirmations, and reschedule itself if the fee is not -// confirmed yet. If the fee tx is ever removed from the wallet, this will -// schedule another reconcile. -func (fp *feePayment) confirmPayment() (err error) { - ctx := fp.ctx - w := fp.client.wallet - - // stop processing if ticket is expired or spent - if fp.removedExpiredOrSpent() { - // nothing scheduled - return errStopped - } - - defer func() { - if err != nil && !errors.Is(err, errStopped) { - fp.schedule("reconcile payment", fp.reconcilePayment) - } - }() - - status, err := fp.client.status(ctx, &fp.ticketHash) - if err != nil { - log.Warnf("Rescheduling status check for %v: %v", &fp.ticketHash, err) - fp.schedule("confirm payment", fp.confirmPayment) - return nil - } - - switch status.FeeTxStatus { - case "received": - // VSP has received the fee tx but has not yet broadcast it. - // VSP will only broadcast the tx when ticket has 6+ confirmations. - fp.schedule("confirm payment", fp.confirmPayment) - return nil - case "broadcast": - log.Infof("VSP has successfully sent the fee tx for %v", &fp.ticketHash) - // Broadcasted, but not confirmed. - fp.schedule("confirm payment", fp.confirmPayment) - return nil - case "confirmed": - fp.remove("confirmed by VSP") - // nothing scheduled - fp.mu.Lock() - feeHash := fp.feeHash - fp.mu.Unlock() - err = w.UpdateVspTicketFeeToConfirmed(ctx, &fp.ticketHash, &feeHash, fp.client.URL, fp.client.PubKey) - if err != nil { - return err - } - return nil - case "error": - log.Warnf("VSP failed to broadcast feetx for %v -- restarting payment", - &fp.ticketHash) - fp.schedule("reconcile payment", fp.reconcilePayment) - return nil - default: - // XXX put in unknown state - log.Warnf("VSP responded with unknown FeeTxStatus %q for %v", - status.FeeTxStatus, &fp.ticketHash) - } - - return nil -} - -func marshalTx(tx *wire.MsgTx) (string, error) { - var buf bytes.Buffer - buf.Grow(tx.SerializeSize() * 2) - err := tx.Serialize(hex.NewEncoder(&buf)) - return buf.String(), err -} diff --git a/libwallet/internal/vsp/vsp.go b/libwallet/internal/vsp/vsp.go index a81dce4ab..8e0e222f2 100644 --- a/libwallet/internal/vsp/vsp.go +++ b/libwallet/internal/vsp/vsp.go @@ -8,9 +8,9 @@ import ( "net/url" "sync" - "decred.org/dcrwallet/v3/errors" - "decred.org/dcrwallet/v3/wallet" - "decred.org/dcrwallet/v3/wallet/udb" + "decred.org/dcrwallet/v4/errors" + "decred.org/dcrwallet/v4/wallet" + "decred.org/dcrwallet/v4/wallet/udb" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/dcrutil/v4" diff --git a/libwallet/log.go b/libwallet/log.go index b6effc459..8811f3b8a 100644 --- a/libwallet/log.go +++ b/libwallet/log.go @@ -8,7 +8,7 @@ package libwallet import ( "os" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/crypto-power/cryptopower/libwallet/internal/loader" "github.com/crypto-power/cryptopower/libwallet/internal/politeia" "github.com/crypto-power/cryptopower/libwallet/internal/vsp" diff --git a/libwallet/ltc.go b/libwallet/ltc.go index 3e1abe71c..1435d9858 100644 --- a/libwallet/ltc.go +++ b/libwallet/ltc.go @@ -3,7 +3,7 @@ package libwallet import ( "fmt" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/crypto-power/cryptopower/libwallet/assets/ltc" sharedW "github.com/crypto-power/cryptopower/libwallet/assets/wallet" "github.com/crypto-power/cryptopower/libwallet/utils" diff --git a/libwallet/txhelper/helper.go b/libwallet/txhelper/helper.go index 3243e1803..b780f078f 100644 --- a/libwallet/txhelper/helper.go +++ b/libwallet/txhelper/helper.go @@ -3,7 +3,7 @@ package txhelper import ( "math" - "decred.org/dcrwallet/v3/wallet" + "decred.org/dcrwallet/v4/wallet" "github.com/decred/dcrd/dcrutil/v4" "github.com/decred/dcrd/wire" "github.com/decred/dcrdata/v8/txhelpers" diff --git a/libwallet/utils/errors.go b/libwallet/utils/errors.go index 73b2862bd..39ab4ef11 100644 --- a/libwallet/utils/errors.go +++ b/libwallet/utils/errors.go @@ -5,7 +5,7 @@ import ( "net" "strings" - "decred.org/dcrwallet/v3/errors" + "decred.org/dcrwallet/v4/errors" "github.com/asdine/storm" ) diff --git a/log.go b/log.go index ba6b36de1..9996a43c2 100644 --- a/log.go +++ b/log.go @@ -34,11 +34,11 @@ import ( "github.com/crypto-power/cryptopower/ui/page/transaction" "github.com/crypto-power/cryptopower/wallet" - "decred.org/dcrwallet/v3/p2p" - "decred.org/dcrwallet/v3/spv" - "decred.org/dcrwallet/v3/ticketbuyer" - dcrw "decred.org/dcrwallet/v3/wallet" - "decred.org/dcrwallet/v3/wallet/udb" + "decred.org/dcrwallet/v4/p2p" + "decred.org/dcrwallet/v4/spv" + "decred.org/dcrwallet/v4/ticketbuyer" + dcrw "decred.org/dcrwallet/v4/wallet" + "decred.org/dcrwallet/v4/wallet/udb" "github.com/btcsuite/btclog" btcC "github.com/btcsuite/btcwallet/chain" btcw "github.com/btcsuite/btcwallet/wallet" diff --git a/ui/page/staking/stake_overview.go b/ui/page/staking/stake_overview.go index 6983f727c..cacc6ff55 100644 --- a/ui/page/staking/stake_overview.go +++ b/ui/page/staking/stake_overview.go @@ -353,8 +353,7 @@ func (pg *Page) HandleUserInteractions() { return } - account := ticketTx.Inputs[0].AccountNumber - err = ticketInfo.Client.ProcessTicket(pg.ctx, txHash, pg.dcrImpl.GetvspPolicy(account)) + err = ticketInfo.Client.Process(pg.ctx, txHash, nil) if err != nil { log.Errorf("processing the unconfirmed tx fee failed: %v", err) }