client: Automatic fee payment from dcrwallet (#382)

This commit is contained in:
Jamie Holdstock 2023-05-30 11:50:23 +01:00 committed by GitHub
parent 799041a1e5
commit a5003c046b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1580 additions and 36 deletions

View File

@ -27,3 +27,18 @@ linters:
- unparam - unparam
- unused - unused
- vetshadow - vetshadow
linters-settings:
# Disable rule SA1019 on staticcheck, it causes the build to fail if any
# deprecated func/var/const are referenced.
staticcheck:
checks: ["all", "-SA1019"]
exhaustive:
check:
- switch
- map
# Presence of "default" case in switch statements satisfies exhaustiveness,
# even if all enum members are not listed.
# Default: false
default-signifies-exhaustive: true

447
client/autoclient.go Normal file
View File

@ -0,0 +1,447 @@
// Copyright (c) 2022-2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package client
import (
"context"
"encoding/base64"
"fmt"
"net"
"net/http"
"net/url"
"sync"
"decred.org/dcrwallet/v3/errors"
"decred.org/dcrwallet/v3/wallet"
"decred.org/dcrwallet/v3/wallet/udb"
"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/wire"
"github.com/decred/slog"
)
type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
type Policy struct {
MaxFee dcrutil.Amount
ChangeAcct uint32 // to derive fee addresses
FeeAcct uint32 // to pay fees from, if inputs are not provided to Process
}
// Ensure dcrwallet satisfies the Wallet interface.
var _ Wallet = (*wallet.Wallet)(nil)
type Wallet interface {
Spender(ctx context.Context, out *wire.OutPoint) (*wire.MsgTx, uint32, error)
MainChainTip(ctx context.Context) (hash chainhash.Hash, height int32)
ChainParams() *chaincfg.Params
TxBlock(ctx context.Context, hash *chainhash.Hash) (chainhash.Hash, int32, error)
DumpWIFPrivateKey(ctx context.Context, addr stdaddr.Address) (string, error)
VSPFeeHashForTicket(ctx context.Context, ticketHash *chainhash.Hash) (chainhash.Hash, error)
UpdateVspTicketFeeToStarted(ctx context.Context, ticketHash, feeHash *chainhash.Hash, host string, pubkey []byte) error
GetTransactionsByHashes(ctx context.Context, txHashes []*chainhash.Hash) (txs []*wire.MsgTx, notFound []*wire.InvVect, err error)
ReserveOutputsForAmount(ctx context.Context, account uint32, amount dcrutil.Amount, minconf int32) ([]wallet.Input, error)
UnlockOutpoint(txHash *chainhash.Hash, index uint32)
NewChangeAddress(ctx context.Context, account uint32) (stdaddr.Address, error)
RelayFee() dcrutil.Amount
SignTransaction(ctx context.Context, tx *wire.MsgTx, hashType txscript.SigHashType, additionalPrevScripts map[wire.OutPoint][]byte,
additionalKeysByAddress map[string]*dcrutil.WIF, p2shRedeemScriptsByAddress map[string][]byte) ([]wallet.SignatureError, error)
SetPublished(ctx context.Context, hash *chainhash.Hash, published bool) error
AddTransaction(ctx context.Context, tx *wire.MsgTx, blockHash *chainhash.Hash) error
UpdateVspTicketFeeToPaid(ctx context.Context, ticketHash, feeHash *chainhash.Hash, host string, pubkey []byte) error
UpdateVspTicketFeeToErrored(ctx context.Context, ticketHash *chainhash.Hash, host string, pubkey []byte) error
AgendaChoices(ctx context.Context, ticketHash *chainhash.Hash) (choices wallet.AgendaChoices, voteBits uint16, err error)
TSpendPolicyForTicket(ticketHash *chainhash.Hash) map[string]string
TreasuryKeyPolicyForTicket(ticketHash *chainhash.Hash) map[string]string
AbandonTransaction(ctx context.Context, hash *chainhash.Hash) error
TxConfirms(ctx context.Context, hash *chainhash.Hash) (int32, error)
ForUnspentUnexpiredTickets(ctx context.Context, f func(hash *chainhash.Hash) error) error
IsVSPTicketConfirmed(ctx context.Context, ticketHash *chainhash.Hash) (bool, error)
UpdateVspTicketFeeToConfirmed(ctx context.Context, ticketHash, feeHash *chainhash.Hash, host string, pubkey []byte) error
VSPTicketInfo(ctx context.Context, ticketHash *chainhash.Hash) (*wallet.VSPTicket, error)
SignMessage(ctx context.Context, msg string, addr stdaddr.Address) (sig []byte, err error)
}
type AutoClient struct {
wallet Wallet
policy *Policy
*Client
mu sync.Mutex
jobs map[chainhash.Hash]*feePayment
log slog.Logger
}
type Config struct {
// URL specifies the base URL of the VSP
URL string
// PubKey specifies the VSP's base64 encoded public key
PubKey string
// Dialer specifies an optional dialer when connecting to the VSP.
Dialer DialFunc
// Wallet specifies a loaded wallet.
Wallet Wallet
// Default policy for fee payments unless another is provided by the
// caller.
Policy *Policy
}
func New(cfg Config, log slog.Logger) (*AutoClient, error) {
u, err := url.Parse(cfg.URL)
if err != nil {
return nil, err
}
pubKey, err := base64.StdEncoding.DecodeString(cfg.PubKey)
if err != nil {
return nil, err
}
if cfg.Wallet == nil {
return nil, fmt.Errorf("wallet option not set")
}
client := &Client{
URL: u.String(),
PubKey: pubKey,
Sign: cfg.Wallet.SignMessage,
Log: log,
}
client.Transport = &http.Transport{
DialContext: cfg.Dialer,
}
v := &AutoClient{
wallet: cfg.Wallet,
policy: cfg.Policy,
Client: client,
jobs: make(map[chainhash.Hash]*feePayment),
log: log,
}
return v, nil
}
func (c *AutoClient) FeePercentage(ctx context.Context) (float64, error) {
resp, err := c.Client.VspInfo(ctx)
if err != nil {
return -1, err
}
return resp.FeePercentage, nil
}
// ProcessUnprocessedTickets processes all tickets that don't currently have
// any association with a VSP.
func (c *AutoClient) ProcessUnprocessedTickets(ctx context.Context) {
var wg sync.WaitGroup
_ = c.wallet.ForUnspentUnexpiredTickets(ctx, func(hash *chainhash.Hash) error {
// Skip tickets which have a fee tx already associated with
// them; they are already processed by some vsp.
_, err := c.wallet.VSPFeeHashForTicket(ctx, hash)
if err == nil {
return nil
}
confirmed, err := c.wallet.IsVSPTicketConfirmed(ctx, hash)
if err != nil && !errors.Is(err, errors.NotExist) {
c.log.Error(err)
return nil
}
if confirmed {
return nil
}
c.mu.Lock()
fp := c.jobs[*hash]
c.mu.Unlock()
if fp != nil {
// Already processing this ticket with the VSP.
return nil
}
// Start processing in the background.
wg.Add(1)
go func() {
defer wg.Done()
err := c.Process(ctx, hash, nil)
if err != nil {
c.log.Error(err)
}
}()
return nil
})
wg.Wait()
}
// ProcessTicket attempts to process a given ticket based on the hash provided.
func (c *AutoClient) ProcessTicket(ctx context.Context, hash *chainhash.Hash) error {
err := c.Process(ctx, hash, nil)
if err != nil {
return err
}
return nil
}
// ProcessManagedTickets discovers tickets which were previously registered with
// a VSP and begins syncing them in the background. This is used to recover VSP
// tracking after seed restores, and is only performed on unspent and unexpired
// tickets.
func (c *AutoClient) ProcessManagedTickets(ctx context.Context) error {
err := c.wallet.ForUnspentUnexpiredTickets(ctx, func(hash *chainhash.Hash) error {
// We only want to process tickets that haven't been confirmed yet.
confirmed, err := c.wallet.IsVSPTicketConfirmed(ctx, hash)
if err != nil && !errors.Is(err, errors.NotExist) {
c.log.Error(err)
return nil
}
if confirmed {
return nil
}
c.mu.Lock()
_, ok := c.jobs[*hash]
c.mu.Unlock()
if ok {
// Already processing this ticket with the VSP.
return nil
}
// Make ticketstatus api call and only continue if ticket is
// found managed by this vsp. The rest is the same codepath as
// for processing a new ticket.
status, err := c.status(ctx, hash)
if err != nil {
if errors.Is(err, errors.Locked) {
return err
}
return nil
}
if status.FeeTxStatus == "confirmed" {
feeHash, err := chainhash.NewHashFromStr(status.FeeTxHash)
if err != nil {
return err
}
err = c.wallet.UpdateVspTicketFeeToConfirmed(ctx, hash, feeHash, c.Client.URL, c.Client.PubKey)
if err != nil {
return err
}
return nil
} else if status.FeeTxHash != "" {
feeHash, err := chainhash.NewHashFromStr(status.FeeTxHash)
if err != nil {
return err
}
err = c.wallet.UpdateVspTicketFeeToPaid(ctx, hash, feeHash, c.Client.URL, c.Client.PubKey)
if err != nil {
return err
}
_ = c.feePayment(ctx, hash, true)
} else {
// Fee hasn't been paid at the provided VSP, so this should do that if needed.
_ = c.feePayment(ctx, hash, false)
}
return nil
})
return err
}
// Process begins processing a VSP fee payment for a ticket. If feeTx contains
// inputs, is used to pay the VSP fee. Otherwise, new inputs are selected and
// locked to prevent double spending the fee.
//
// feeTx must not be nil, but may point to an empty transaction, and is modified
// with the inputs and the fee and change outputs before returning without an
// error. The fee transaction is also recorded as unpublised in the wallet, and
// the fee hash is associated with the ticket.
func (c *AutoClient) Process(ctx context.Context, ticketHash *chainhash.Hash, feeTx *wire.MsgTx) error {
vspTicket, err := c.wallet.VSPTicketInfo(ctx, ticketHash)
if err != nil && !errors.Is(err, errors.NotExist) {
return err
}
feeStatus := udb.VSPFeeProcessStarted // Will be used if the ticket isn't registered to the vsp yet.
if vspTicket != nil {
feeStatus = udb.FeeStatus(vspTicket.FeeTxStatus)
}
switch feeStatus {
case udb.VSPFeeProcessStarted, udb.VSPFeeProcessErrored:
// If VSPTicket has been started or errored then attempt to create a new fee
// transaction, submit it then confirm.
fp := c.feePayment(ctx, ticketHash, false)
if fp == nil {
err := c.wallet.UpdateVspTicketFeeToErrored(ctx, ticketHash, c.Client.URL, c.Client.PubKey)
if err != nil {
return err
}
return fmt.Errorf("fee payment cannot be processed")
}
fp.mu.Lock()
if fp.feeTx == nil {
fp.feeTx = feeTx
}
fp.mu.Unlock()
err := fp.receiveFeeAddress()
if err != nil {
err := c.wallet.UpdateVspTicketFeeToErrored(ctx, ticketHash, c.Client.URL, c.Client.PubKey)
if err != nil {
return err
}
// XXX, retry? (old Process retried)
// but this may not be necessary any longer as the parent of
// the ticket is always relayed to the vsp as well.
return err
}
err = fp.makeFeeTx(feeTx)
if err != nil {
err := c.wallet.UpdateVspTicketFeeToErrored(ctx, ticketHash, c.Client.URL, c.Client.PubKey)
if err != nil {
return err
}
return err
}
return fp.submitPayment()
case udb.VSPFeeProcessPaid:
// If a VSP ticket has been paid, but confirm payment.
if len(vspTicket.Host) > 0 && vspTicket.Host != c.Client.URL {
// Cannot confirm a paid ticket that is already with another VSP.
return fmt.Errorf("ticket already paid or confirmed with another vsp")
}
fp := c.feePayment(ctx, ticketHash, true)
if fp == nil {
// Don't update VSPStatus to Errored if it was already paid or
// confirmed.
return fmt.Errorf("fee payment cannot be processed")
}
return fp.confirmPayment()
case udb.VSPFeeProcessConfirmed:
// VSPTicket has already been confirmed, there is nothing to process.
return nil
}
return nil
}
// SetVoteChoice takes the provided consensus, tspend and treasury key voting
// preferences, and checks if they match the status of the specified ticket from
// the connected VSP. The status provides the current voting preferences so we
// can just update from there if need be.
func (c *AutoClient) SetVoteChoice(ctx context.Context, hash *chainhash.Hash,
choices map[string]string, tspendPolicy map[string]string, treasuryPolicy map[string]string) error {
// Retrieve current voting preferences from VSP.
status, err := c.status(ctx, hash)
if err != nil {
if errors.Is(err, errors.Locked) {
return err
}
c.log.Errorf("Could not check status of VSP ticket %s: %v", hash, err)
return nil
}
// Check for any mismatch between the provided voting preferences and the
// VSP preferences to determine if VSP needs to be updated.
update := false
// Check consensus vote choices.
for newAgenda, newChoice := range choices {
vspChoice, ok := status.VoteChoices[newAgenda]
if !ok {
update = true
break
}
if vspChoice != newChoice {
update = true
break
}
}
// Check tspend policies.
for newTSpend, newChoice := range tspendPolicy {
vspChoice, ok := status.TSpendPolicy[newTSpend]
if !ok {
update = true
break
}
if vspChoice != newChoice {
update = true
break
}
}
// Check treasury policies.
for newKey, newChoice := range treasuryPolicy {
vspChoice, ok := status.TSpendPolicy[newKey]
if !ok {
update = true
break
}
if vspChoice != newChoice {
update = true
break
}
}
if !update {
c.log.Debugf("VSP already has correct vote choices for ticket %s", hash)
return nil
}
c.log.Debugf("Updating vote choices on VSP for ticket %s", hash)
err = c.setVoteChoices(ctx, hash, choices, tspendPolicy, treasuryPolicy)
if err != nil {
return err
}
return nil
}
// TicketInfo stores per-ticket info tracked by a VSP Client instance.
type TicketInfo struct {
TicketHash chainhash.Hash
CommitmentAddr stdaddr.StakeAddress
VotingAddr stdaddr.StakeAddress
State State
Fee dcrutil.Amount
FeeHash chainhash.Hash
// TODO: include stuff returned by the status() call?
}
// TrackedTickets returns information about all outstanding tickets tracked by
// a vsp.Client instance.
//
// Currently this returns only info about tickets which fee hasn't been paid or
// confirmed at enough depth to be considered committed to.
func (c *AutoClient) TrackedTickets() []*TicketInfo {
// Collect all jobs first, to avoid working under two different locks.
c.mu.Lock()
jobs := make([]*feePayment, 0, len(c.jobs))
for _, job := range c.jobs {
jobs = append(jobs, job)
}
c.mu.Unlock()
tickets := make([]*TicketInfo, 0, len(jobs))
for _, job := range jobs {
job.mu.Lock()
tickets = append(tickets, &TicketInfo{
TicketHash: job.ticketHash,
CommitmentAddr: job.commitmentAddr,
VotingAddr: job.votingAddr,
State: job.state,
Fee: job.fee,
FeeHash: job.feeHash,
})
job.mu.Unlock()
}
return tickets
}

View File

@ -1,3 +1,7 @@
// Copyright (c) 2022-2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package client package client
import ( import (

View File

@ -1,3 +1,7 @@
// Copyright (c) 2022-2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package client package client
import ( import (

937
client/feepayment.go Normal file
View File

@ -0,0 +1,937 @@
// Copyright (c) 2022-2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package client
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"math/big"
"sync"
"time"
wallet_errs "decred.org/dcrwallet/v3/errors"
"decred.org/dcrwallet/v3/wallet"
"decred.org/dcrwallet/v3/wallet/txrules"
"decred.org/dcrwallet/v3/wallet/txsizes"
"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"
"github.com/decred/slog"
"github.com/decred/vspd/types/v2"
)
// randomInt64 returns a random int64 in [0,n).
func randomInt64(n int64) int64 {
i, err := rand.Int(rand.Reader, big.NewInt(n))
if err != nil {
// crypto/rand should never return an error if running on a supported platform.
panic(fmt.Sprintf("unhandled crypto/rand error: %v", err))
}
return i.Int64()
}
// randomDuration returns a random time.Duration in [0,d).
func randomDuration(d time.Duration) time.Duration {
return time.Duration(randomInt64(int64(d)))
}
// coinflip returns a random bool.
func coinflip() bool {
return randomInt64(2) == 0
}
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 *AutoClient
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
log slog.Logger
}
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()
fp.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 *AutoClient) 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
params := w.ChainParams()
fp = &feePayment{
client: c,
ctx: ctx,
ticketHash: *ticketHash,
policy: c.policy,
log: c.log,
}
// 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.log.Warnf("no ticket found for %v", ticketHash)
return nil
}
_, ticketHeight, err := w.TxBlock(ctx, ticketHash)
if err != nil {
// This is not expected to ever error, as the ticket was fetched
// from the wallet in the above call.
fp.log.Errorf("failed to query block which mines ticket: %v", err)
return nil
}
if ticketHeight >= 2 {
// Note the off-by-one; this is correct. Tickets become live
// one block after the params would indicate.
fp.ticketLive = ticketHeight + int32(params.TicketMaturity) + 1
fp.ticketExpires = fp.ticketLive + int32(params.TicketExpiry)
}
fp.votingAddr, fp.commitmentAddr, err = parseTicket(ticket, params)
if err != nil {
fp.log.Errorf("%v is not a ticket: %v", ticketHash, err)
return nil
}
// Try to access the voting key, ignore error unless the wallet is
// locked.
fp.votingKey, err = w.DumpWIFPrivateKey(ctx, fp.votingAddr)
if err != nil && !errors.Is(err, wallet_errs.Locked) {
fp.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 *AutoClient) 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 {
fp.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 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 {
fp.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)
}
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, params)
if err != nil {
return fmt.Errorf("server fee address invalid: %w", err)
}
fp.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 be 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 {
fp.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 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 {
fp.log.Errorf("failed to sign transaction: %v", err)
sigErrStr := ""
for _, sigErr := range sigErrs {
fp.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.PubKey)
if err != nil {
return err
}
fp.mu.Lock()
fp.feeTx = tx
fp.feeHash = feeHash
fp.mu.Unlock()
// nothing scheduled
return nil
}
func (c *AutoClient) status(ctx context.Context, ticketHash *chainhash.Hash) (*types.TicketStatusResponse, 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)
}
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 *AutoClient) setVoteChoices(ctx context.Context, ticketHash *chainhash.Hash,
choices map[string]string, 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)
}
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.
default:
// do nothing.
}
}
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
}
// 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
}
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 {
fp.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?
fp.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, wallet_errs.Locked) {
fp.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.PubKey)
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":
fp.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":
fp.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
fp.log.Warnf("VSP responded with %v 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
}

View File

@ -3,20 +3,47 @@ module github.com/decred/vspd/client/v2
go 1.19 go 1.19
require ( require (
decred.org/dcrwallet/v3 v3.0.0-20230519033517-96817277627d
github.com/decred/dcrd/blockchain/stake/v5 v5.0.0
github.com/decred/dcrd/chaincfg/chainhash v1.0.4
github.com/decred/dcrd/chaincfg/v3 v3.1.2
github.com/decred/dcrd/dcrutil/v4 v4.0.0
github.com/decred/dcrd/txscript/v4 v4.0.0 github.com/decred/dcrd/txscript/v4 v4.0.0
github.com/decred/dcrd/wire v1.5.0
github.com/decred/slog v1.2.0 github.com/decred/slog v1.2.0
github.com/decred/vspd/types/v2 v2.0.0 github.com/decred/vspd/types/v2 v2.0.0
) )
replace (
github.com/decred/dcrd/blockchain/stake/v5 => github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92
github.com/decred/dcrd/blockchain/standalone/v2 => github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af
github.com/decred/dcrd/chaincfg/v3 => github.com/decred/dcrd/chaincfg/v3 v3.1.2-0.20230412145739-9aa79ec168f6
github.com/decred/dcrd/gcs/v4 => github.com/decred/dcrd/gcs/v4 v4.0.0-20221022042529-0a0cc3b3bf92
)
require ( require (
decred.org/cspp/v2 v2.0.0 // indirect
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
github.com/dchest/siphash v1.2.2 // indirect github.com/companyzero/sntrup4591761 v0.0.0-20200131011700-2b0d299dbd22 // indirect
github.com/decred/base58 v1.0.3 // indirect github.com/dchest/siphash v1.2.3 // indirect
github.com/decred/dcrd/chaincfg/chainhash v1.0.3 // indirect github.com/decred/base58 v1.0.4 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/crypto/ripemd160 v1.0.1 // indirect github.com/decred/dcrd/crypto/ripemd160 v1.0.1 // indirect
github.com/decred/dcrd/database/v3 v3.0.0 // indirect
github.com/decred/dcrd/dcrec v1.0.0 // indirect github.com/decred/dcrd/dcrec v1.0.0 // indirect
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 // indirect github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/decred/dcrd/wire v1.5.0 // indirect github.com/decred/dcrd/dcrjson/v4 v4.0.1 // indirect
github.com/decred/dcrd/gcs/v4 v4.0.0 // indirect
github.com/decred/dcrd/hdkeychain/v3 v3.1.0 // indirect
github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0 // indirect
github.com/decred/go-socks v1.1.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/jrick/bitset v1.0.0 // indirect
github.com/jrick/wsrpc/v2 v2.3.5 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.5.0 // indirect
) )

View File

@ -1,31 +1,140 @@
decred.org/cspp/v2 v2.0.0 h1:b4fZrElRufz30rYnBZ2shhC8AjNVTN4i6TMzDi+hk44=
decred.org/cspp/v2 v2.0.0/go.mod h1:0shJWKTWY3LxZEWGxtbER1Y45+HVjC0WZtj4bctSzCI=
decred.org/dcrwallet/v3 v3.0.0-20230519033517-96817277627d h1:Gov8SXYPsw6eyIBhzLP6+kjfDHdv6Q/ifIMt8fKtoYc=
decred.org/dcrwallet/v3 v3.0.0-20230519033517-96817277627d/go.mod h1:JGNFyWBroylUUf973eIHsdHN7PuJdCUVkIOMyWVv+y0=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
github.com/companyzero/sntrup4591761 v0.0.0-20200131011700-2b0d299dbd22 h1:vfqLMkB1UqwJliW0I/34oscQawInrVfL1uPjGEEt2YY=
github.com/companyzero/sntrup4591761 v0.0.0-20200131011700-2b0d299dbd22/go.mod h1:LoZJNGDWmVPqMEHmeJzj4Weq4Stjc6FKY6FVpY3Hem0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/siphash v1.2.2 h1:9DFz8tQwl9pTVt5iok/9zKyzA1Q6bRGiF3HPiEEVr9I=
github.com/dchest/siphash v1.2.2/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4= github.com/dchest/siphash v1.2.2/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
github.com/decred/base58 v1.0.3 h1:KGZuh8d1WEMIrK0leQRM47W85KqCAdl2N+uagbctdDI= github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E= github.com/decred/base58 v1.0.3/go.mod h1:pXP9cXCfM2sFLb2viz2FNIdeMWmZDBKG3ZBYbiSM78E=
github.com/decred/base58 v1.0.4 h1:QJC6B0E0rXOPA8U/kw2rP+qiRJsUaE2Er+pYb3siUeA=
github.com/decred/base58 v1.0.4/go.mod h1:jJswKPEdvpFpvf7dsDvFZyLT22xZ9lWqEByX38oGd9E=
github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92 h1:GTIg6r54cgNhUyZMNmTsmxM8OneEJ8t6QcWQCqu+al0=
github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92/go.mod h1:fij5xS9IBfJ5e/F5ytp/g/TWjrETEMXUFlE6C7KYOvA=
github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af h1:tmfTIkxkq7NdeLj3mrMk0uEdRyU1/oO1MWx8dfwOmm8=
github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af/go.mod h1:PpM/jdMaD5MnBcSoFd+rJZE4q8tU0xPTTAyVzgegLQI=
github.com/decred/dcrd/blockchain/v5 v5.0.0-20221022042529-0a0cc3b3bf92 h1:AYgHfuWXQh5NTv3mciQleTm2K1IuM7ozgfXy1Vg6V7k=
github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60=
github.com/decred/dcrd/chaincfg/chainhash v1.0.3 h1:PF2czcYZGW3dz4i/35AUfVAgnqHl9TMNQt1ADTYGOoE=
github.com/decred/dcrd/chaincfg/chainhash v1.0.3/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= github.com/decred/dcrd/chaincfg/chainhash v1.0.3/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60=
github.com/decred/dcrd/chaincfg/v3 v3.1.0 h1:u8l+E6ryv8E0WY69pM/lUI36UeAVcLKBwD/Q3xPiuog= github.com/decred/dcrd/chaincfg/chainhash v1.0.4 h1:zRCv6tdncLfLTKYqu7hrXvs7hW+8FO/NvwoFvGsrluU=
github.com/decred/dcrd/chaincfg/v3 v3.1.0/go.mod h1:4XF9nlx2NeGD4xzw1+L0DGICZMl0a5rKV8nnuHLgk8o= github.com/decred/dcrd/chaincfg/chainhash v1.0.4/go.mod h1:hA86XxlBWwHivMvxzXTSD0ZCG/LoYsFdWnCekkTMCqY=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/chaincfg/v3 v3.1.2-0.20230412145739-9aa79ec168f6 h1:rKAzrv3gIEQaEQpnWU4IKxXrvx6QfXkdiOUKLvwEpQw=
github.com/decred/dcrd/chaincfg/v3 v3.1.2-0.20230412145739-9aa79ec168f6/go.mod h1:aEEti0kQSBFAlzHln4FB+3L30k9ZN1M7YDfYuK5VWtc=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/crypto/ripemd160 v1.0.1 h1:TjRL4LfftzTjXzaufov96iDAkbY2R3aTvH2YMYa1IOc= github.com/decred/dcrd/crypto/ripemd160 v1.0.1 h1:TjRL4LfftzTjXzaufov96iDAkbY2R3aTvH2YMYa1IOc=
github.com/decred/dcrd/crypto/ripemd160 v1.0.1/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg= github.com/decred/dcrd/crypto/ripemd160 v1.0.1/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg=
github.com/decred/dcrd/database/v3 v3.0.0 h1:7VVN2sWjKB934jvXzjnyGJFUVH9d8Qh5VULi+NMRjek=
github.com/decred/dcrd/database/v3 v3.0.0/go.mod h1:8EyKddB8rXDi6/CDOdYc/7qL1//sb6iwg9DctP0ZJF4=
github.com/decred/dcrd/dcrec v1.0.0 h1:W+z6Es+Rai3MXYVoPAxYr5U1DGis0Co33scJ6uH2J6o= github.com/decred/dcrd/dcrec v1.0.0 h1:W+z6Es+Rai3MXYVoPAxYr5U1DGis0Co33scJ6uH2J6o=
github.com/decred/dcrd/dcrec v1.0.0/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIfmePklt8Q8= github.com/decred/dcrd/dcrec v1.0.0/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIfmePklt8Q8=
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 h1:bX7rtGTMBDJxujZ29GNqtn7YCAdINjHKnA6J6tBBv6s= github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 h1:bX7rtGTMBDJxujZ29GNqtn7YCAdINjHKnA6J6tBBv6s=
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc= github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
github.com/decred/dcrd/dcrjson/v4 v4.0.1 h1:vyQuB1miwGqbCVNm8P6br3V65WQ6wyrh0LycMkvaBBg=
github.com/decred/dcrd/dcrjson/v4 v4.0.1/go.mod h1:2qVikafVF9/X3PngQVmqkbUbyAl32uik0k/kydgtqMc=
github.com/decred/dcrd/dcrutil/v4 v4.0.0 h1:AY00fWy/ETrMHN0DNV3XUbH1aip2RG1AoTy5dp0+sJE=
github.com/decred/dcrd/dcrutil/v4 v4.0.0/go.mod h1:QQpX5WVH3/ixVtiW15xZMe+neugXX3l2bsrYgq6nz4M=
github.com/decred/dcrd/gcs/v4 v4.0.0-20221022042529-0a0cc3b3bf92 h1:DiAdpAQg54JL5iWFdB+DEdK/xuKIjTyL6mlR7iIumPQ=
github.com/decred/dcrd/gcs/v4 v4.0.0-20221022042529-0a0cc3b3bf92/go.mod h1:2SpSpCW0vOWlACQNAn7mPuIb3Vet070zfs1SpcSEv8o=
github.com/decred/dcrd/hdkeychain/v3 v3.1.0 h1:NlUjzPMzexbk1PyJu6vrQaiilep5WsEPB0KdhLYrEcE=
github.com/decred/dcrd/hdkeychain/v3 v3.1.0/go.mod h1:rDCdqwGkcTfEyRheG1g8Wc38appT2C9+D1XTlLy21lo=
github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0 h1:4YUKsWKrKlkhVMYGRB6G0XI6QfwUnwEH18eoEbM1/+M=
github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0/go.mod h1:dDHO7ivrPAhZjFD3LoOJN/kdq5gi0sxie6zCsWHAiUo=
github.com/decred/dcrd/txscript/v4 v4.0.0 h1:BwaBUCMCmg58MCYoBhxVjL8ZZKUIfoJuxu/djmh8h58= github.com/decred/dcrd/txscript/v4 v4.0.0 h1:BwaBUCMCmg58MCYoBhxVjL8ZZKUIfoJuxu/djmh8h58=
github.com/decred/dcrd/txscript/v4 v4.0.0/go.mod h1:OJtxNc5RqwQyfrRnG2gG8uMeNPo8IAJp+TD1UKXkqk8= github.com/decred/dcrd/txscript/v4 v4.0.0/go.mod h1:OJtxNc5RqwQyfrRnG2gG8uMeNPo8IAJp+TD1UKXkqk8=
github.com/decred/dcrd/wire v1.5.0 h1:3SgcEzSjqAMQvOugP0a8iX7yQSpiVT1yNi9bc4iOXVg= github.com/decred/dcrd/wire v1.5.0 h1:3SgcEzSjqAMQvOugP0a8iX7yQSpiVT1yNi9bc4iOXVg=
github.com/decred/dcrd/wire v1.5.0/go.mod h1:fzAjVqw32LkbAZIt5mnrvBR751GTa3e0rRQdOIhPY3w= github.com/decred/dcrd/wire v1.5.0/go.mod h1:fzAjVqw32LkbAZIt5mnrvBR751GTa3e0rRQdOIhPY3w=
github.com/decred/go-socks v1.1.0 h1:dnENcc0KIqQo3HSXdgboXAHgqsCIutkqq6ntQjYtm2U=
github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0=
github.com/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM= github.com/decred/slog v1.2.0 h1:soHAxV52B54Di3WtKLfPum9OFfWqwtf/ygf9njdfnPM=
github.com/decred/slog v1.2.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0= github.com/decred/slog v1.2.0/go.mod h1:kVXlGnt6DHy2fV5OjSeuvCJ0OmlmTF6LFpEPMu/fOY0=
github.com/decred/vspd/types/v2 v2.0.0 h1:FaPA+W4OOMRWK+Vk4fyyYdXoVLRMMRQsxzsnSjJjOnI= github.com/decred/vspd/types/v2 v2.0.0 h1:FaPA+W4OOMRWK+Vk4fyyYdXoVLRMMRQsxzsnSjJjOnI=
github.com/decred/vspd/types/v2 v2.0.0/go.mod h1:2xnNqedkt9GuL+pK8uIzDxqYxFlwLRflYFJH64b76n0= github.com/decred/vspd/types/v2 v2.0.0/go.mod h1:2xnNqedkt9GuL+pK8uIzDxqYxFlwLRflYFJH64b76n0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/bitset v1.0.0 h1:Ws0PXV3PwXqWK2n7Vz6idCdrV/9OrBXgHEJi27ZB9Dw=
github.com/jrick/bitset v1.0.0/go.mod h1:ZOYB5Uvkla7wIEY4FEssPVi3IQXa02arznRaYaAEPe4=
github.com/jrick/wsrpc/v2 v2.3.4/go.mod h1:XPYs8BnRWl99lCvXRM5SLpZmTPqWpSOPkDIqYTwDPfU=
github.com/jrick/wsrpc/v2 v2.3.5 h1:CwdycaR/df09iGkPMXs1FxqAHMCQbdAiTGoHfOrtuds=
github.com/jrick/wsrpc/v2 v2.3.5/go.mod h1:7oBeDM/xMF6Yqy4GDAjpppuOf1hm6lWsaG3EaMrm+aA=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

16
go.mod
View File

@ -6,9 +6,9 @@ require (
decred.org/dcrwallet/v3 v3.0.0 decred.org/dcrwallet/v3 v3.0.0
github.com/decred/dcrd/blockchain/stake/v5 v5.0.0 github.com/decred/dcrd/blockchain/stake/v5 v5.0.0
github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1 github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1
github.com/decred/dcrd/chaincfg/chainhash v1.0.3 github.com/decred/dcrd/chaincfg/chainhash v1.0.4
github.com/decred/dcrd/chaincfg/v3 v3.1.1 github.com/decred/dcrd/chaincfg/v3 v3.1.2
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
github.com/decred/dcrd/dcrutil/v4 v4.0.0 github.com/decred/dcrd/dcrutil/v4 v4.0.0
github.com/decred/dcrd/hdkeychain/v3 v3.1.0 github.com/decred/dcrd/hdkeychain/v3 v3.1.0
github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0 github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0
@ -28,23 +28,23 @@ require (
) )
replace ( replace (
decred.org/dcrwallet/v3 => decred.org/dcrwallet/v3 v3.0.0-20230406144806-dc82294b976a decred.org/dcrwallet/v3 => decred.org/dcrwallet/v3 v3.0.0-20230519033517-96817277627d
github.com/decred/dcrd/blockchain/stake/v5 => github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92 github.com/decred/dcrd/blockchain/stake/v5 => github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92
github.com/decred/dcrd/blockchain/standalone/v2 => github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230411184711-ce46220cf772 github.com/decred/dcrd/blockchain/standalone/v2 => github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af
github.com/decred/dcrd/chaincfg/v3 => github.com/decred/dcrd/chaincfg/v3 v3.1.2-0.20230412145739-9aa79ec168f6
github.com/decred/dcrd/gcs/v4 => github.com/decred/dcrd/gcs/v4 v4.0.0-20221022042529-0a0cc3b3bf92 github.com/decred/dcrd/gcs/v4 => github.com/decred/dcrd/gcs/v4 v4.0.0-20221022042529-0a0cc3b3bf92
github.com/decred/dcrd/rpc/jsonrpc/types/v4 => github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0-20221022042529-0a0cc3b3bf92
) )
require ( require (
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
github.com/dchest/siphash v1.2.3 // indirect github.com/dchest/siphash v1.2.3 // indirect
github.com/decred/base58 v1.0.4 // indirect github.com/decred/base58 v1.0.4 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/crypto/ripemd160 v1.0.1 // indirect github.com/decred/dcrd/crypto/ripemd160 v1.0.1 // indirect
github.com/decred/dcrd/database/v3 v3.0.0 // indirect github.com/decred/dcrd/database/v3 v3.0.0 // indirect
github.com/decred/dcrd/dcrec v1.0.0 // indirect github.com/decred/dcrd/dcrec v1.0.0 // indirect
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 // indirect github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 // indirect
github.com/decred/dcrd/dcrjson/v4 v4.0.0 // indirect github.com/decred/dcrd/dcrjson/v4 v4.0.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect

31
go.sum
View File

@ -1,5 +1,5 @@
decred.org/dcrwallet/v3 v3.0.0-20230406144806-dc82294b976a h1:gssWH7pL1maf4qOGt2vkIcaXvz8GVAvR9sltU6W7JOQ= decred.org/dcrwallet/v3 v3.0.0-20230519033517-96817277627d h1:Gov8SXYPsw6eyIBhzLP6+kjfDHdv6Q/ifIMt8fKtoYc=
decred.org/dcrwallet/v3 v3.0.0-20230406144806-dc82294b976a/go.mod h1:hXxn7XBmvCZQ4D0uMdMVCzAsV258WoDIl+/UrABEB4o= decred.org/dcrwallet/v3 v3.0.0-20230519033517-96817277627d/go.mod h1:JGNFyWBroylUUf973eIHsdHN7PuJdCUVkIOMyWVv+y0=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -14,16 +14,17 @@ github.com/decred/base58 v1.0.4 h1:QJC6B0E0rXOPA8U/kw2rP+qiRJsUaE2Er+pYb3siUeA=
github.com/decred/base58 v1.0.4/go.mod h1:jJswKPEdvpFpvf7dsDvFZyLT22xZ9lWqEByX38oGd9E= github.com/decred/base58 v1.0.4/go.mod h1:jJswKPEdvpFpvf7dsDvFZyLT22xZ9lWqEByX38oGd9E=
github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92 h1:GTIg6r54cgNhUyZMNmTsmxM8OneEJ8t6QcWQCqu+al0= github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92 h1:GTIg6r54cgNhUyZMNmTsmxM8OneEJ8t6QcWQCqu+al0=
github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92/go.mod h1:fij5xS9IBfJ5e/F5ytp/g/TWjrETEMXUFlE6C7KYOvA= github.com/decred/dcrd/blockchain/stake/v5 v5.0.0-20221022042529-0a0cc3b3bf92/go.mod h1:fij5xS9IBfJ5e/F5ytp/g/TWjrETEMXUFlE6C7KYOvA=
github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230411184711-ce46220cf772 h1:LNE2EluIv1R4R933HJhS8rpgOqfpPAeAKkPq0Bf7cP8= github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af h1:tmfTIkxkq7NdeLj3mrMk0uEdRyU1/oO1MWx8dfwOmm8=
github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230411184711-ce46220cf772/go.mod h1:PpM/jdMaD5MnBcSoFd+rJZE4q8tU0xPTTAyVzgegLQI= github.com/decred/dcrd/blockchain/standalone/v2 v2.1.1-0.20230430213532-f95870f9c6af/go.mod h1:PpM/jdMaD5MnBcSoFd+rJZE4q8tU0xPTTAyVzgegLQI=
github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60=
github.com/decred/dcrd/chaincfg/chainhash v1.0.3 h1:PF2czcYZGW3dz4i/35AUfVAgnqHl9TMNQt1ADTYGOoE=
github.com/decred/dcrd/chaincfg/chainhash v1.0.3/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60= github.com/decred/dcrd/chaincfg/chainhash v1.0.3/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D9NuaFd+zGyNeIKgrhCXK60=
github.com/decred/dcrd/chaincfg/v3 v3.1.0/go.mod h1:4XF9nlx2NeGD4xzw1+L0DGICZMl0a5rKV8nnuHLgk8o= github.com/decred/dcrd/chaincfg/chainhash v1.0.4 h1:zRCv6tdncLfLTKYqu7hrXvs7hW+8FO/NvwoFvGsrluU=
github.com/decred/dcrd/chaincfg/v3 v3.1.1 h1:Ki8kq5IXGmjriiQyPCrCTF1aZSBiORb91/Sr5xW4otw= github.com/decred/dcrd/chaincfg/chainhash v1.0.4/go.mod h1:hA86XxlBWwHivMvxzXTSD0ZCG/LoYsFdWnCekkTMCqY=
github.com/decred/dcrd/chaincfg/v3 v3.1.1/go.mod h1:4XF9nlx2NeGD4xzw1+L0DGICZMl0a5rKV8nnuHLgk8o= github.com/decred/dcrd/chaincfg/v3 v3.1.2-0.20230412145739-9aa79ec168f6 h1:rKAzrv3gIEQaEQpnWU4IKxXrvx6QfXkdiOUKLvwEpQw=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/chaincfg/v3 v3.1.2-0.20230412145739-9aa79ec168f6/go.mod h1:aEEti0kQSBFAlzHln4FB+3L30k9ZN1M7YDfYuK5VWtc=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/crypto/ripemd160 v1.0.1 h1:TjRL4LfftzTjXzaufov96iDAkbY2R3aTvH2YMYa1IOc= github.com/decred/dcrd/crypto/ripemd160 v1.0.1 h1:TjRL4LfftzTjXzaufov96iDAkbY2R3aTvH2YMYa1IOc=
github.com/decred/dcrd/crypto/ripemd160 v1.0.1/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg= github.com/decred/dcrd/crypto/ripemd160 v1.0.1/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg=
github.com/decred/dcrd/database/v3 v3.0.0 h1:7VVN2sWjKB934jvXzjnyGJFUVH9d8Qh5VULi+NMRjek= github.com/decred/dcrd/database/v3 v3.0.0 h1:7VVN2sWjKB934jvXzjnyGJFUVH9d8Qh5VULi+NMRjek=
@ -33,16 +34,16 @@ github.com/decred/dcrd/dcrec v1.0.0/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIf
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 h1:bX7rtGTMBDJxujZ29GNqtn7YCAdINjHKnA6J6tBBv6s= github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2 h1:bX7rtGTMBDJxujZ29GNqtn7YCAdINjHKnA6J6tBBv6s=
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc= github.com/decred/dcrd/dcrec/edwards/v2 v2.0.2/go.mod h1:d0H8xGMWbiIQP7gN3v2rByWUcuZPm9YsgmnfoxgbINc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/decred/dcrd/dcrjson/v4 v4.0.0 h1:KsaFhHAYO+vLYz7Qmx/fs1gOY5ouTEz8hRuDm8jmJtU= github.com/decred/dcrd/dcrjson/v4 v4.0.1 h1:vyQuB1miwGqbCVNm8P6br3V65WQ6wyrh0LycMkvaBBg=
github.com/decred/dcrd/dcrjson/v4 v4.0.0/go.mod h1:DMnSpU8lsVh+Nt5kHl63tkrjBDA7UIs4+ov8Kwwgvjs= github.com/decred/dcrd/dcrjson/v4 v4.0.1/go.mod h1:2qVikafVF9/X3PngQVmqkbUbyAl32uik0k/kydgtqMc=
github.com/decred/dcrd/dcrutil/v4 v4.0.0 h1:AY00fWy/ETrMHN0DNV3XUbH1aip2RG1AoTy5dp0+sJE= github.com/decred/dcrd/dcrutil/v4 v4.0.0 h1:AY00fWy/ETrMHN0DNV3XUbH1aip2RG1AoTy5dp0+sJE=
github.com/decred/dcrd/dcrutil/v4 v4.0.0/go.mod h1:QQpX5WVH3/ixVtiW15xZMe+neugXX3l2bsrYgq6nz4M= github.com/decred/dcrd/dcrutil/v4 v4.0.0/go.mod h1:QQpX5WVH3/ixVtiW15xZMe+neugXX3l2bsrYgq6nz4M=
github.com/decred/dcrd/hdkeychain/v3 v3.1.0 h1:NlUjzPMzexbk1PyJu6vrQaiilep5WsEPB0KdhLYrEcE= github.com/decred/dcrd/hdkeychain/v3 v3.1.0 h1:NlUjzPMzexbk1PyJu6vrQaiilep5WsEPB0KdhLYrEcE=
github.com/decred/dcrd/hdkeychain/v3 v3.1.0/go.mod h1:rDCdqwGkcTfEyRheG1g8Wc38appT2C9+D1XTlLy21lo= github.com/decred/dcrd/hdkeychain/v3 v3.1.0/go.mod h1:rDCdqwGkcTfEyRheG1g8Wc38appT2C9+D1XTlLy21lo=
github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0-20221022042529-0a0cc3b3bf92 h1:R32+XN8qM6kB7qUHfbkskPAldTPXWIhxMk+e3UBNwyY= github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0 h1:4YUKsWKrKlkhVMYGRB6G0XI6QfwUnwEH18eoEbM1/+M=
github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0-20221022042529-0a0cc3b3bf92/go.mod h1:x1zG8D4HRmUCDtAioDe/QQ/PazzFXcYtSrbasX3FHoE= github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0/go.mod h1:dDHO7ivrPAhZjFD3LoOJN/kdq5gi0sxie6zCsWHAiUo=
github.com/decred/dcrd/txscript/v4 v4.0.0 h1:BwaBUCMCmg58MCYoBhxVjL8ZZKUIfoJuxu/djmh8h58= github.com/decred/dcrd/txscript/v4 v4.0.0 h1:BwaBUCMCmg58MCYoBhxVjL8ZZKUIfoJuxu/djmh8h58=
github.com/decred/dcrd/txscript/v4 v4.0.0/go.mod h1:OJtxNc5RqwQyfrRnG2gG8uMeNPo8IAJp+TD1UKXkqk8= github.com/decred/dcrd/txscript/v4 v4.0.0/go.mod h1:OJtxNc5RqwQyfrRnG2gG8uMeNPo8IAJp+TD1UKXkqk8=
github.com/decred/dcrd/wire v1.5.0 h1:3SgcEzSjqAMQvOugP0a8iX7yQSpiVT1yNi9bc4iOXVg= github.com/decred/dcrd/wire v1.5.0 h1:3SgcEzSjqAMQvOugP0a8iX7yQSpiVT1yNi9bc4iOXVg=