client: Automatic fee payment from dcrwallet (#382)
This commit is contained in:
parent
799041a1e5
commit
a5003c046b
@ -27,3 +27,18 @@ linters:
|
||||
- unparam
|
||||
- unused
|
||||
- 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
447
client/autoclient.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
import (
|
||||
|
||||
@ -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
|
||||
|
||||
import (
|
||||
|
||||
937
client/feepayment.go
Normal file
937
client/feepayment.go
Normal 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
|
||||
}
|
||||
@ -3,20 +3,47 @@ module github.com/decred/vspd/client/v2
|
||||
go 1.19
|
||||
|
||||
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/wire v1.5.0
|
||||
github.com/decred/slog v1.2.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 (
|
||||
decred.org/cspp/v2 v2.0.0 // indirect
|
||||
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
|
||||
github.com/dchest/siphash v1.2.2 // indirect
|
||||
github.com/decred/base58 v1.0.3 // indirect
|
||||
github.com/decred/dcrd/chaincfg/chainhash v1.0.3 // indirect
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
|
||||
github.com/companyzero/sntrup4591761 v0.0.0-20200131011700-2b0d299dbd22 // indirect
|
||||
github.com/dchest/siphash v1.2.3 // indirect
|
||||
github.com/decred/base58 v1.0.4 // 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/database/v3 v3.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/secp256k1/v4 v4.0.1 // indirect
|
||||
github.com/decred/dcrd/wire v1.5.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.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
|
||||
)
|
||||
|
||||
123
client/go.sum
123
client/go.sum
@ -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/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/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/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.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.3 h1:PF2czcYZGW3dz4i/35AUfVAgnqHl9TMNQt1ADTYGOoE=
|
||||
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/v3 v3.1.0/go.mod h1:4XF9nlx2NeGD4xzw1+L0DGICZMl0a5rKV8nnuHLgk8o=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
|
||||
github.com/decred/dcrd/chaincfg/chainhash v1.0.4 h1:zRCv6tdncLfLTKYqu7hrXvs7hW+8FO/NvwoFvGsrluU=
|
||||
github.com/decred/dcrd/chaincfg/chainhash v1.0.4/go.mod h1:hA86XxlBWwHivMvxzXTSD0ZCG/LoYsFdWnCekkTMCqY=
|
||||
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.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/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/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/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.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/go.mod h1:OJtxNc5RqwQyfrRnG2gG8uMeNPo8IAJp+TD1UKXkqk8=
|
||||
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/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/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/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
16
go.mod
@ -6,9 +6,9 @@ require (
|
||||
decred.org/dcrwallet/v3 v3.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/chaincfg/chainhash v1.0.3
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.1.1
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0
|
||||
github.com/decred/dcrd/chaincfg/chainhash v1.0.4
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.1.2
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
|
||||
github.com/decred/dcrd/dcrutil/v4 v4.0.0
|
||||
github.com/decred/dcrd/hdkeychain/v3 v3.1.0
|
||||
github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0
|
||||
@ -28,23 +28,23 @@ require (
|
||||
)
|
||||
|
||||
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/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/rpc/jsonrpc/types/v4 => github.com/decred/dcrd/rpc/jsonrpc/types/v4 v4.0.0-20221022042529-0a0cc3b3bf92
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect
|
||||
github.com/dchest/siphash v1.2.3 // 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/database/v3 v3.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/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/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
|
||||
31
go.sum
31
go.sum
@ -1,5 +1,5 @@
|
||||
decred.org/dcrwallet/v3 v3.0.0-20230406144806-dc82294b976a h1:gssWH7pL1maf4qOGt2vkIcaXvz8GVAvR9sltU6W7JOQ=
|
||||
decred.org/dcrwallet/v3 v3.0.0-20230406144806-dc82294b976a/go.mod h1:hXxn7XBmvCZQ4D0uMdMVCzAsV258WoDIl+/UrABEB4o=
|
||||
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/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
|
||||
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/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.20230411184711-ce46220cf772 h1:LNE2EluIv1R4R933HJhS8rpgOqfpPAeAKkPq0Bf7cP8=
|
||||
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 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/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/v3 v3.1.0/go.mod h1:4XF9nlx2NeGD4xzw1+L0DGICZMl0a5rKV8nnuHLgk8o=
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.1.1 h1:Ki8kq5IXGmjriiQyPCrCTF1aZSBiORb91/Sr5xW4otw=
|
||||
github.com/decred/dcrd/chaincfg/v3 v3.1.1/go.mod h1:4XF9nlx2NeGD4xzw1+L0DGICZMl0a5rKV8nnuHLgk8o=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
|
||||
github.com/decred/dcrd/chaincfg/chainhash v1.0.4 h1:zRCv6tdncLfLTKYqu7hrXvs7hW+8FO/NvwoFvGsrluU=
|
||||
github.com/decred/dcrd/chaincfg/chainhash v1.0.4/go.mod h1:hA86XxlBWwHivMvxzXTSD0ZCG/LoYsFdWnCekkTMCqY=
|
||||
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.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/go.mod h1:F0H8cjIuWTRoixr/LM3REB8obcWkmYx0gbxpQWR8RPg=
|
||||
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/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.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.0 h1:KsaFhHAYO+vLYz7Qmx/fs1gOY5ouTEz8hRuDm8jmJtU=
|
||||
github.com/decred/dcrd/dcrjson/v4 v4.0.0/go.mod h1:DMnSpU8lsVh+Nt5kHl63tkrjBDA7UIs4+ov8Kwwgvjs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
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/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-20221022042529-0a0cc3b3bf92 h1:R32+XN8qM6kB7qUHfbkskPAldTPXWIhxMk+e3UBNwyY=
|
||||
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 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/go.mod h1:OJtxNc5RqwQyfrRnG2gG8uMeNPo8IAJp+TD1UKXkqk8=
|
||||
github.com/decred/dcrd/wire v1.5.0 h1:3SgcEzSjqAMQvOugP0a8iX7yQSpiVT1yNi9bc4iOXVg=
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user