vspd/rpc/dcrwallet.go
jholdstock 1e66b6ff59 Downgrade dcrwallet dep to v3.
This downgrade changes StakePoolTicketFee back to the version which does
not consider DCP-0012 activation.

This resolves an issue where Decrediton sometimes fails to pay VSP fees,
caused by Decrediton and vspd independently calculating the fee amount
using different versions of the algorithm.

Releasing the new algorithm will need to be more carefully coordinated,
potentially requiring both client and server sides to be updated in
sync.
2023-11-08 09:01:36 +00:00

241 lines
7.5 KiB
Go

// Copyright (c) 2021-2023 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package rpc
import (
"context"
"fmt"
wallettypes "decred.org/dcrwallet/v3/rpc/jsonrpc/types"
"github.com/decred/dcrd/chaincfg/v3"
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v4"
"github.com/decred/dcrd/wire"
"github.com/decred/slog"
)
var (
requiredWalletVersion = semver{Major: 9, Minor: 0, Patch: 0}
)
// WalletRPC provides methods for calling dcrwallet JSON-RPCs without exposing the details
// of JSON encoding.
type WalletRPC struct {
Caller
}
type WalletConnect struct {
clients []*client
params *chaincfg.Params
log slog.Logger
}
func SetupWallet(user, pass, addrs []string, cert [][]byte, params *chaincfg.Params, log slog.Logger) WalletConnect {
clients := make([]*client, len(addrs))
for i := 0; i < len(addrs); i++ {
clients[i] = setup(user[i], pass[i], addrs[i], cert[i], log)
}
return WalletConnect{
clients: clients,
params: params,
log: log,
}
}
func (w *WalletConnect) Close() {
for _, client := range w.clients {
client.Close()
}
w.log.Debug("dcrwallet clients closed")
}
// Clients loops over each wallet and tries to establish a connection. It
// increments a count of failed connections if a connection cannot be
// established, or if the wallet is misconfigured.
func (w *WalletConnect) Clients() ([]*WalletRPC, []string) {
ctx := context.TODO()
walletClients := make([]*WalletRPC, 0)
failedConnections := make([]string, 0)
for _, connect := range w.clients {
c, newConnection, err := connect.dial(ctx)
if err != nil {
w.log.Errorf("dcrwallet dial error: %v", err)
failedConnections = append(failedConnections, connect.addr)
continue
}
// If this is a reused connection, we don't need to validate the
// dcrwallet config again.
if !newConnection {
walletClients = append(walletClients, &WalletRPC{c})
continue
}
// Verify dcrwallet is at the required api version.
var verMap map[string]dcrdtypes.VersionResult
err = c.Call(ctx, "version", &verMap)
if err != nil {
w.log.Errorf("dcrwallet.Version error (wallet=%s): %v", c.String(), err)
failedConnections = append(failedConnections, connect.addr)
connect.Close()
continue
}
ver, exists := verMap["dcrwalletjsonrpcapi"]
if !exists {
w.log.Errorf("dcrwallet.Version response missing 'dcrwalletjsonrpcapi' (wallet=%s)",
c.String())
failedConnections = append(failedConnections, connect.addr)
connect.Close()
continue
}
sVer := semver{ver.Major, ver.Minor, ver.Patch}
if !semverCompatible(requiredWalletVersion, sVer) {
w.log.Errorf("dcrwallet has incompatible JSON-RPC version (wallet=%s): got %s, expected %s",
c.String(), sVer, requiredWalletVersion)
failedConnections = append(failedConnections, connect.addr)
connect.Close()
continue
}
// Verify dcrwallet is on the correct network.
var netID wire.CurrencyNet
err = c.Call(ctx, "getcurrentnet", &netID)
if err != nil {
w.log.Errorf("dcrwallet.GetCurrentNet error (wallet=%s): %v", c.String(), err)
failedConnections = append(failedConnections, connect.addr)
connect.Close()
continue
}
if netID != w.params.Net {
w.log.Errorf("dcrwallet on wrong network (wallet=%s): running on %s, expected %s",
c.String(), netID, w.params.Net)
failedConnections = append(failedConnections, connect.addr)
connect.Close()
continue
}
// Verify dcrwallet is voting and unlocked.
walletRPC := &WalletRPC{c}
walletInfo, err := walletRPC.WalletInfo()
if err != nil {
w.log.Errorf("dcrwallet.WalletInfo error (wallet=%s): %v", c.String(), err)
failedConnections = append(failedConnections, connect.addr)
connect.Close()
continue
}
if !walletInfo.ManualTickets {
// All wallet should not be adding tickets found via the network. This
// misconfiguration should not have a negative impact on users, so just
// log an error here. Don't count this as a failed connection.
w.log.Errorf("wallet does not have manual tickets enabled (wallet=%s)", c.String())
}
if !walletInfo.Voting {
// All wallet RPCs can still be used if voting is disabled, so just
// log an error here. Don't count this as a failed connection.
w.log.Errorf("wallet is not voting (wallet=%s)", c.String())
}
if !walletInfo.Unlocked {
// SetVoteChoice can still be used even if the wallet is locked, so
// just log an error here. Don't count this as a failed connection.
w.log.Errorf("wallet is not unlocked (wallet=%s)", c.String())
}
walletClients = append(walletClients, walletRPC)
}
return walletClients, failedConnections
}
// WalletInfo uses walletinfo RPC to retrieve information about how the
// dcrwallet instance is configured.
func (c *WalletRPC) WalletInfo() (*wallettypes.WalletInfoResult, error) {
var walletInfo wallettypes.WalletInfoResult
err := c.Call(context.TODO(), "walletinfo", &walletInfo)
if err != nil {
return nil, err
}
return &walletInfo, nil
}
// AddTicketForVoting uses importprivkey RPC, followed by addtransaction RPC, to
// add a new ticket to a voting wallet.
func (c *WalletRPC) AddTicketForVoting(votingWIF, blockHash, txHex string) error {
const label = "imported"
const rescan = false
const scanFrom = 0
err := c.Call(context.TODO(), "importprivkey", nil, votingWIF, label, rescan, scanFrom)
if err != nil {
return fmt.Errorf("importprivkey failed: %w", err)
}
err = c.Call(context.TODO(), "addtransaction", nil, blockHash, txHex)
if err != nil {
return fmt.Errorf("addtransaction failed: %w", err)
}
return nil
}
// SetVoteChoice uses setvotechoice RPC to set the vote choice on the given
// agenda, for the given ticket.
func (c *WalletRPC) SetVoteChoice(agenda, choice, ticketHash string) error {
return c.Call(context.TODO(), "setvotechoice", nil, agenda, choice, ticketHash)
}
// GetBestBlockHeight uses getblockcount RPC to query the height of the best
// block known by the dcrwallet instance.
func (c *WalletRPC) GetBestBlockHeight() (int64, error) {
var height int64
err := c.Call(context.TODO(), "getblockcount", &height)
if err != nil {
return 0, err
}
return height, nil
}
// TicketInfo uses ticketinfo RPC to retrieve a detailed list of all tickets
// known by this dcrwallet instance.
func (c *WalletRPC) TicketInfo(startHeight int64) (map[string]*wallettypes.TicketInfoResult, error) {
var result []*wallettypes.TicketInfoResult
err := c.Call(context.TODO(), "ticketinfo", &result, startHeight)
if err != nil {
return nil, err
}
// For easier access later on, store the tickets in a map using their hash
// as the key.
tickets := make(map[string]*wallettypes.TicketInfoResult, len(result))
for _, t := range result {
tickets[t.Hash] = t
}
return tickets, err
}
// RescanFrom uses rescanwallet RPC to trigger the wallet to perform a rescan
// from the specified block height.
func (c *WalletRPC) RescanFrom(fromHeight int64) error {
return c.Call(context.TODO(), "rescanwallet", nil, fromHeight)
}
// SetTreasuryPolicy sets the specified tickets voting policy for all tspends
// published by the given treasury key.
func (c *WalletRPC) SetTreasuryPolicy(key, policy, ticket string) error {
return c.Call(context.TODO(), "settreasurypolicy", nil, key, policy, ticket)
}
// SetTSpendPolicy sets the specified tickets voting policy for a single tspend
// identified by its hash.
func (c *WalletRPC) SetTSpendPolicy(tSpend, policy, ticket string) error {
return c.Call(context.TODO(), "settspendpolicy", nil, tSpend, policy, ticket)
}