vspd/rpc/dcrwallet.go
jholdstock 74729c6cc9 multi: Consider DCP0012 in VSP fee calculations.
Upgrade the dcrwallet dependency to pick up the new version of
txrules.StakePoolTicketFee which considers the status of DCP0012 in its
fee calculation.
2023-09-16 08:36:41 +01: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/v4/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)
}