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 Jul 13, 2023
1 parent ecafbf3 commit ce32862
Show file tree
Hide file tree
Showing 17 changed files with 1,195 additions and 93 deletions.
162 changes: 162 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 @@ -555,6 +561,14 @@ type mempoolRedeem struct {
firstSeen time.Time
}

// 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 @@ -586,6 +600,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 @@ -607,6 +623,11 @@ 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
vspInfo func(url string) (*vspdjson.VspInfoResponse, error)

connected atomic.Bool
}

func (dcr *ExchangeWallet) config() *exchangeWalletConfig {
Expand Down Expand Up @@ -684,6 +705,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 @@ -825,6 +847,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, fmt.Errorf("unable to create wallet dir: %v", err)
}

w := &ExchangeWallet{
log: logger,
chainParams: chainParams,
Expand All @@ -836,6 +863,22 @@ 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,
vspInfo: vspInfo,
walletType: cfg.Type,
}

if b, err := os.ReadFile(filepath.Join(dir, vspFileName)); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("unable to read vsp file: %v", err)
}
} else {
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)
}

w.cfgV.Store(walletCfg)
Expand Down Expand Up @@ -900,6 +943,8 @@ func (dcr *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error)
defer func() {
if !success {
dcr.wallet.Disconnect()
} else {
dcr.connected.Store(true)
}
}()

Expand Down Expand Up @@ -4546,6 +4591,123 @@ func (dcr *ExchangeWallet) EstimateSendTxFee(address string, sendAmount, feeRate
return finalFee, isValidAddress, nil
}

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

func (dcr *ExchangeWallet) StakeStatus() (*asset.TicketStakingStatus, error) {
if !dcr.connected.Load() {
return nil, errors.New("not connected, login first")
}
sdiff, err := dcr.wallet.StakeDiff(dcr.ctx)
if err != nil {
return nil, err
}
isRPC := !dcr.isInternal()
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 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. 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.isInternal() {
return dcr.wallet.PurchaseTickets(dcr.ctx, n, "", "")
}
v := dcr.vspV.Load()
if v == nil {
return nil, errors.New("no vsp set")
}
return dcr.wallet.PurchaseTickets(dcr.ctx, n, v.(*vsp).Url, v.(*vsp).PubKey)
}

// 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 {
if !dcr.connected.Load() {
return errors.New("not connected, login first")
}
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
Loading

0 comments on commit ce32862

Please sign in to comment.