Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client/dcr: Add staking methods. #2290

Merged
merged 1 commit into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/asset/dcr/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type walletConfig struct {
RedeemConfTarget uint64 `ini:"redeemconftarget"`
ActivelyUsed bool `ini:"special_activelyUsed"` //injected by core
ApiFeeFallback bool `ini:"apifeefallback"`
VSPURL string `ini:"vspurl"`
}

type rpcConfig struct {
Expand Down
159 changes: 159 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 @@ -94,6 +98,8 @@ const (
// monitored until this number of confirms is reached. Two to make sure
// the block containing the redeem is stakeholder-approved
requiredRedeemConfirms = 2

vspFileName = "vsp.json"
)

var (
Expand Down Expand Up @@ -557,6 +563,14 @@ type mempoolRedeem struct {
firstSeen time.Time
}

// vsp holds info needed for purchasing tickets from a vsp. PubKey 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 @@ -588,6 +602,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 @@ -609,6 +625,10 @@ type ExchangeWallet struct {
// TODO: Consider persisting mempool redeems on file.
mempoolRedeemsMtx sync.RWMutex
mempoolRedeems map[[32]byte]*mempoolRedeem // keyed by secret hash

vspV atomic.Value // *vsp

connected atomic.Bool
}

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

type block struct {
height int64
Expand Down Expand Up @@ -827,6 +848,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 {
JoeGruffins marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("unable to create wallet dir: %v", err)
}

w := &ExchangeWallet{
log: logger,
chainParams: chainParams,
Expand All @@ -838,6 +864,19 @@ func unconnectedWallet(cfg *asset.WalletConfig, dcrCfg *walletConfig, chainParam
externalTxCache: make(map[chainhash.Hash]*externalTx),
oracleFees: make(map[uint64]feeStamped),
mempoolRedeems: make(map[[32]byte]*mempoolRedeem),
dir: dir,
walletType: cfg.Type,
}

if b, err := os.ReadFile(filepath.Join(dir, vspFileName)); err == nil {
var v vsp
err = json.Unmarshal(b, &v)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal vsp file: %v", err)
}
w.vspV.Store(&v)
} else if !errors.Is(err, os.ErrNotExist) {
JoeGruffins marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("unable to read vsp file: %v", err)
}

w.cfgV.Store(walletCfg)
Expand Down Expand Up @@ -925,6 +964,7 @@ func (dcr *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error)
}

success = true // All good, don't disconnect the wallet when this method returns.
dcr.connected.Store(true)

// NotifyOnTipChange will return false if the wallet does not support
// tip change notification. We'll use dcr.monitorBlocks below if so.
Expand Down Expand Up @@ -5134,6 +5174,125 @@ func (dcr *ExchangeWallet) EstimateSendTxFee(address string, sendAmount, feeRate
return finalFee, isValidAddress, nil
}

func (dcr *ExchangeWallet) isSPV() bool {
return dcr.walletType == walletTypeSPV
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reminds me. There's a dcr.wallet.SpvMode() which may be true even for a wallet connected via rpc. Put another way, isSPV could be true even if walletType isn't walletTypeSPV.

}

func (dcr *ExchangeWallet) StakeStatus() (*asset.TicketStakingStatus, error) {
if !dcr.connected.Load() {
return nil, errors.New("not connected, login first")
}
Comment on lines +5182 to +5184
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering what these dcr.connected checks do for us. We could theoretically add this check to almost any ExchangeWallet method, but we don't because we expect the caller to know better, and continuing without being connected would generate an error anyway. What would happen if we didn't check this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My memory is fuzzy, but these will panic if the dcr.ctx is nil, which is so before the first connect.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I added this to any methods that use dcr.ctx to avoid using a nil context and panic inside the rpcclient. Should I handle this differently?

sdiff, err := dcr.wallet.StakeDiff(dcr.ctx)
if err != nil {
return nil, err
}
isRPC := !dcr.isSPV()
var vspURL string
if !isRPC {
if v := dcr.vspV.Load(); v != nil {
vspURL = v.(*vsp).URL
}
}
tickets, err := dcr.wallet.Tickets(dcr.ctx)
if err != nil {
return nil, fmt.Errorf("error retrieving tickets: %w", err)
}
voteChoices, tSpendPolicy, treasuryPolicy, err := dcr.wallet.VotingPreferences(dcr.ctx)
if err != nil {
return nil, fmt.Errorf("error retrieving stances: %w", err)
}
return &asset.TicketStakingStatus{
TicketPrice: uint64(sdiff),
VSP: vspURL,
IsRPC: isRPC,
Tickets: tickets,
Stances: asset.Stances{
VoteChoices: voteChoices,
TSpendPolicy: tSpendPolicy,
TreasuryPolicy: treasuryPolicy,
},
}, nil
}

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 can be checked with StakeStatus
// first. Only non-RPC (internal) wallets can be set. Part of the
// asset.TicketBuyer interface.
func (dcr *ExchangeWallet) SetVSP(url string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems SetVSP and other TicketBuyer methods are not consumed anywhere, except in simnet_test.go, this means consumption of these new methods/features would be in a follow-up PR, no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. With the rpcserver in #2316 and webserver not done yet.

if !dcr.isSPV() {
return errors.New("cannot set vsp for external wallet")
}
info, err := 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 number of tickets. Part of the asset.TicketBuyer
// interface.
func (dcr *ExchangeWallet) PurchaseTickets(n int) ([]string, error) {
if n < 1 {
return nil, nil
}
if !dcr.connected.Load() {
return nil, errors.New("not connected, login first")
}
if !dcr.isSPV() {
return dcr.wallet.PurchaseTickets(dcr.ctx, n, "", "")
}
v := dcr.vspV.Load()
if v == nil {
return nil, errors.New("no vsp set")
}
vInfo := v.(*vsp)
return dcr.wallet.PurchaseTickets(dcr.ctx, n, vInfo.URL, vInfo.PubKey)
}

// SetVotingPreferences sets the vote choices 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 {
if !dcr.connected.Load() {
return errors.New("not connected, login first")
}
return dcr.wallet.SetVotingPreferences(dcr.ctx, choices, tspendPolicy, treasuryPolicy)
}

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 @@ -518,6 +518,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
Loading