Skip to content

Commit

Permalink
client/dcr: Add staking methods.
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeGruffins committed Apr 13, 2023
1 parent 709a770 commit be0e3d7
Show file tree
Hide file tree
Showing 29 changed files with 3,210 additions and 28 deletions.
154 changes: 154 additions & 0 deletions client/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
Expand All @@ -15,6 +16,8 @@ import (
"io"
"math"
"net/http"
neturl "net/url"
"os"
"path/filepath"
"sort"
"strconv"
Expand Down Expand Up @@ -44,6 +47,7 @@ import (
"github.com/decred/dcrd/txscript/v4/stdaddr"
"github.com/decred/dcrd/txscript/v4/stdscript"
"github.com/decred/dcrd/wire"
vspdjson "github.com/decred/vspd/types"
)

const (
Expand Down Expand Up @@ -88,6 +92,8 @@ const (
// freshFeeAge is the expiry age for cached fee rates of external origin,
// past which fetchFeeFromOracle should be used to refresh the rate.
freshFeeAge = time.Minute

vspFileName = "vsp.json"
)

var (
Expand Down Expand Up @@ -537,6 +543,14 @@ type exchangeWalletConfig struct {
apiFeeFallback bool
}

// vsp holds info needed for purchasing tickes from a vsp. pub is from the vsp
// and is used for verifying communications.
type vsp struct {
Url string `json:"url"`
FeePercentage float64 `json:"feepercent"`
PubKey string `json:"pubkey"`
}

// ExchangeWallet is a wallet backend for Decred. The backend is how the DEX
// client app communicates with the Decred blockchain and wallet. ExchangeWallet
// satisfies the dex.Wallet interface.
Expand Down Expand Up @@ -568,6 +582,8 @@ type ExchangeWallet struct {
tipChange func(error)
lastPeerCount uint32
peersChange func(uint32, error)
dir string
walletType string

oracleFeesMtx sync.Mutex
oracleFees map[uint64]feeStamped // conf target => fee rate
Expand All @@ -585,6 +601,9 @@ type ExchangeWallet struct {

externalTxMtx sync.RWMutex
externalTxCache map[chainhash.Hash]*externalTx

vspV atomic.Value // *vsp
vspInfo func(url string) (*vspdjson.VspInfoResponse, error)
}

func (dcr *ExchangeWallet) config() *exchangeWalletConfig {
Expand Down Expand Up @@ -661,6 +680,7 @@ var _ asset.Withdrawer = (*ExchangeWallet)(nil)
var _ asset.LiveReconfigurer = (*ExchangeWallet)(nil)
var _ asset.TxFeeEstimator = (*ExchangeWallet)(nil)
var _ asset.Bonder = (*ExchangeWallet)(nil)
var _ asset.TicketBuyer = (*ExchangeWallet)(nil)

type block struct {
height int64
Expand Down Expand Up @@ -802,6 +822,11 @@ func unconnectedWallet(cfg *asset.WalletConfig, dcrCfg *walletConfig, chainParam
return nil, err
}

dir := filepath.Join(cfg.DataDir, chainParams.Name)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, err
}

w := &ExchangeWallet{
log: logger,
chainParams: chainParams,
Expand All @@ -812,6 +837,22 @@ func unconnectedWallet(cfg *asset.WalletConfig, dcrCfg *walletConfig, chainParam
findRedemptionQueue: make(map[outPoint]*findRedemptionReq),
externalTxCache: make(map[chainhash.Hash]*externalTx),
oracleFees: make(map[uint64]feeStamped),
dir: dir,
vspInfo: vspInfo,
walletType: cfg.Type,
}

if b, err := os.ReadFile(filepath.Join(dir, vspFileName)); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
} else {
var v vsp
err = json.Unmarshal(b, &v)
if err != nil {
return nil, err
}
w.vspV.Store(v)
}

w.cfgV.Store(walletCfg)
Expand Down Expand Up @@ -4523,6 +4564,119 @@ func (dcr *ExchangeWallet) EstimateSendTxFee(address string, sendAmount, feeRate
return finalFee, isValidAddress, nil
}

func (dcr *ExchangeWallet) isInternal() bool {
return dcr.walletType == walletTypeSPV
}

// TicketPrice is the current price of one ticket. Also known as the stake
// difficulty. Part of the asset.TicketBuyer interface.
func (dcr *ExchangeWallet) TicketPrice() (uint64, error) {
sdiff, err := dcr.wallet.StakeDiff(dcr.ctx)
if err != nil {
return 0, err
}
return uint64(sdiff), nil
}

// VSP returns the currently set VSP address and fee.
func (dcr *ExchangeWallet) VSP() (addr string, feePercentage float64, err error) {
if !dcr.isInternal() {
return "", 0.0, errors.New("unable to get external VSP stats")
}
v := dcr.vspV.Load().(*vsp)
if v == nil {
return "", 0.0, errors.New("no vsp set")
}
return v.Url, v.FeePercentage, nil
}

// CanSetVSP returns whether the VSP can be changed. It cannot for rpcwallets
// but can for internal. Part of the asset.TicketBuyer interface.
func (dcr *ExchangeWallet) CanSetVSP() bool {
// Cannot set for rpcwallets.
return dcr.isInternal()
}

func vspInfo(url string) (*vspdjson.VspInfoResponse, error) {
suffix := "/api/v3/vspinfo"
path, err := neturl.JoinPath(url, suffix)
if err != nil {
return nil, err
}
resp, err := http.Get(path)
if err != nil {
return nil, fmt.Errorf("http get error: %v", err)
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var info vspdjson.VspInfoResponse
err = json.Unmarshal(b, &info)
if err != nil {
return nil, err
}
return &info, nil
}

// SetVSP sets the VSP provider. Ability to set should be checked with CanSetVSP
// first. Part of the asset.TicketBuyer interface.
func (dcr *ExchangeWallet) SetVSP(url string) error {
if !dcr.isInternal() {
return errors.New("cannot set vsp for external wallet")
}
info, err := dcr.vspInfo(url)
if err != nil {
return err
}
v := vsp{
Url: url,
PubKey: base64.StdEncoding.EncodeToString(info.PubKey),
FeePercentage: info.FeePercentage,
}
b, err := json.Marshal(&v)
if err != nil {
return err
}
if err := os.WriteFile(filepath.Join(dcr.dir, vspFileName), b, 0666); err != nil {
return err
}
dcr.vspV.Store(&v)
return nil
}

// PurchaseTickets purchases n amout of tickets. Ability to purchase should be
// checked with CanPurchaseTickets. Part of the asset.TicketBuyer interface.
func (dcr *ExchangeWallet) PurchaseTickets(n int) error {
if !dcr.isInternal() {
return dcr.wallet.PurchaseTickets(dcr.ctx, n, "", "")
}
v := dcr.vspV.Load().(*vsp)
if v == nil {
return errors.New("no vsp set")
}
return dcr.wallet.PurchaseTickets(dcr.ctx, n, v.Url, v.PubKey)
}

// Tickets returns current active tickets up until they are able to be spent.
// Includes unconfirmed tickets. Part of the asset.TicketBuyer interface.
func (dcr *ExchangeWallet) Tickets() ([]string, error) {
return dcr.wallet.Tickets(dcr.ctx)
}

// VotingPreferences returns current voting preferences. Part of the
// asset.TicketBuyer interface.
func (dcr *ExchangeWallet) VotingPreferences() ([]*walletjson.VoteChoice, []*walletjson.TSpendPolicyResult, []*walletjson.TreasuryPolicyResult, error) {
return dcr.wallet.VotingPreferences(dcr.ctx)
}

// SetVotingPreferences sets default voting settings for all active tickets and
// future tickets. Nil maps can be provided for no change. Part of the
// asset.TicketBuyer interface.
func (dcr *ExchangeWallet) SetVotingPreferences(choices map[string]string, tspendPolicy map[string]string, treasuryPolicy map[string]string) error {
return dcr.wallet.SetVotingPreferences(dcr.ctx, choices, tspendPolicy, treasuryPolicy, dcr.vspInfo)
}

func (dcr *ExchangeWallet) broadcastTx(signedTx *wire.MsgTx) (*chainhash.Hash, error) {
txHash, err := dcr.wallet.SendRawTransaction(dcr.ctx, signedTx, false)
if err != nil {
Expand Down
22 changes: 22 additions & 0 deletions client/asset/dcr/dcr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,28 @@ func (c *tRPCClient) Disconnected() bool {
return c.disconnected
}

func (c *tRPCClient) GetStakeInfo(ctx context.Context) (*walletjson.GetStakeInfoResult, error) {
return nil, nil
}

func (c *tRPCClient) PurchaseTicket(ctx context.Context, fromAccount string, spendLimit dcrutil.Amount, minConf *int,
ticketAddress stdaddr.Address, numTickets *int, poolAddress stdaddr.Address, poolFees *dcrutil.Amount,
expiry *int, ticketChange *bool, ticketFee *dcrutil.Amount) ([]*chainhash.Hash, error) {
return nil, nil
}

func (c *tRPCClient) GetTickets(ctx context.Context, includeImmature bool) ([]*chainhash.Hash, error) {
return nil, nil
}

func (c *tRPCClient) GetVoteChoices(ctx context.Context) (*walletjson.GetVoteChoicesResult, error) {
return nil, nil
}

func (c *tRPCClient) SetVoteChoice(ctx context.Context, agendaID, choiceID string) error {
return nil
}

func (c *tRPCClient) RawRequest(_ context.Context, method string, params []json.RawMessage) (json.RawMessage, error) {
if rr, found := c.rawRes[method]; found {
return rr, c.rawErr[method] // err probably should be nil, but respect the config
Expand Down
99 changes: 99 additions & 0 deletions client/asset/dcr/rpcwallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/decred/dcrd/rpcclient/v7"
"github.com/decred/dcrd/txscript/v4/stdaddr"
"github.com/decred/dcrd/wire"
vspdjson "github.com/decred/vspd/types"
)

var (
Expand Down Expand Up @@ -138,6 +139,13 @@ type rpcClient interface {
RawRequest(ctx context.Context, method string, params []json.RawMessage) (json.RawMessage, error)
WalletInfo(ctx context.Context) (*walletjson.WalletInfoResult, error)
ValidateAddress(ctx context.Context, address stdaddr.Address) (*walletjson.ValidateAddressWalletResult, error)
GetStakeInfo(ctx context.Context) (*walletjson.GetStakeInfoResult, error)
PurchaseTicket(ctx context.Context, fromAccount string, spendLimit dcrutil.Amount, minConf *int,
ticketAddress stdaddr.Address, numTickets *int, poolAddress stdaddr.Address, poolFees *dcrutil.Amount,
expiry *int, ticketChange *bool, ticketFee *dcrutil.Amount) ([]*chainhash.Hash, error)
GetTickets(ctx context.Context, includeImmature bool) ([]*chainhash.Hash, error)
GetVoteChoices(ctx context.Context) (*walletjson.GetVoteChoicesResult, error)
SetVoteChoice(ctx context.Context, agendaID, choiceID string) error
}

// newRPCWallet creates an rpcClient and uses it to construct a new instance
Expand Down Expand Up @@ -864,6 +872,97 @@ func (w *rpcWallet) AddressPrivKey(ctx context.Context, address stdaddr.Address)
return &priv, nil
}

func (w *rpcWallet) StakeDiff(ctx context.Context) (dcrutil.Amount, error) {
si, err := w.rpcClient.GetStakeInfo(ctx)
if err != nil {
return 0, err
}
amt, err := dcrutil.NewAmount(si.Difficulty)
if err != nil {
return 0, err
}
return amt, nil
}

func (w *rpcWallet) PurchaseTickets(ctx context.Context, n int, _, _ string) error {
_, err := w.rpcClient.PurchaseTicket(ctx, "default", 0 /*spendLimit dcrutil.Amount*/, nil, /*minConf *int*/
nil /*ticketAddress stdaddr.Address*/, &n, nil /*poolAddress stdaddr.Address*/, nil, /*poolFees *dcrutil.Amount*/
nil /*expiry *int*/, nil /*ticketChange *bool*/, nil /*ticketFee *dcrutil.Amount*/)
return err
}

func (w *rpcWallet) Tickets(ctx context.Context) ([]string, error) {
const includeImmature = true
// GetTickets only works for clients with a dcrd backend.
hashes, err := w.rpcClient.GetTickets(ctx, includeImmature)
if err != nil {
return nil, err
}
hashStrs := make([]string, 0, len(hashes))
for i := range hashes {
hashStrs = append(hashStrs, hashes[i].String())
}
return hashStrs, nil
}

func (w *rpcWallet) VotingPreferences(ctx context.Context) ([]*walletjson.VoteChoice, []*walletjson.TSpendPolicyResult, []*walletjson.TreasuryPolicyResult, error) {
// Get consensus vote choices.
choices, err := w.rpcClient.GetVoteChoices(ctx)
if err != nil {
return nil, nil, nil, fmt.Errorf("unable to get vote choices: %v", err)
}
voteChoices := make([]*walletjson.VoteChoice, len(choices.Choices))
for i, v := range choices.Choices {
voteChoices[i] = &v
}
// Get tspend voting policy.
const tSpendPolicyMethod = "tspendpolicy"
var tSpendRes []walletjson.TSpendPolicyResult
err = w.rpcClientRawRequest(ctx, tSpendPolicyMethod, nil, &tSpendRes)
if err != nil {
return nil, nil, nil, fmt.Errorf("unable to get treasury spend policy: %v", err)
}
tSpendPolicy := make([]*walletjson.TSpendPolicyResult, len(tSpendRes))
for i, v := range tSpendRes {
tSpendPolicy[i] = &v
}
// Get treasury voting policy.
const treasuryPolicyMethod = "treasurypolicy"
var treasuryRes []walletjson.TreasuryPolicyResult
err = w.rpcClientRawRequest(ctx, treasuryPolicyMethod, nil, &treasuryRes)
if err != nil {
return nil, nil, nil, fmt.Errorf("unable to get treasury policy: %v", err)
}
treasuryPolicy := make([]*walletjson.TreasuryPolicyResult, len(treasuryRes))
for i, v := range treasuryRes {
treasuryPolicy[i] = &v
}
return voteChoices, tSpendPolicy, treasuryPolicy, nil
}

// NOTE: Will fail for communication problems with VSPs unlike internal wallets.
func (w *rpcWallet) SetVotingPreferences(ctx context.Context, choices, tSpendPolicy,
treasuryPolicy map[string]string, _ func(url string) (*vspdjson.VspInfoResponse, error)) error {
for k, v := range choices {
if err := w.rpcClient.SetVoteChoice(ctx, k, v); err != nil {
return fmt.Errorf("unable to set vote choice: %v", err)
}
}
const setTSpendPolicyMethod = "settspendpolicy"
for k, v := range tSpendPolicy {
if err := w.rpcClientRawRequest(ctx, setTSpendPolicyMethod, anylist{k, v}, nil); err != nil {
return fmt.Errorf("unable to set tspend policy: %v", err)
}
}
const setTreasuryPolicyMethod = "settreasurypolicy"
for k, v := range treasuryPolicy {
if err := w.rpcClientRawRequest(ctx, setTreasuryPolicyMethod, anylist{k, v}, nil); err != nil {
return fmt.Errorf("unable to set treasury policy: %v", err)
}
}
return nil
}

// anylist is a list of RPC parameters to be converted to []json.RawMessage and
// sent via nodeRawRequest.
type anylist []interface{}
Expand Down
Loading

0 comments on commit be0e3d7

Please sign in to comment.