decred.org/dcrdex@v1.0.5/tatanka/chain/utxo/dcr.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package utxo
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"math"
    12  	"os"
    13  	"sync"
    14  	"sync/atomic"
    15  	"time"
    16  
    17  	"decred.org/dcrdex/dex"
    18  	"decred.org/dcrdex/tatanka/chain"
    19  	"decred.org/dcrdex/tatanka/tanka"
    20  	"github.com/decred/dcrd/chaincfg/chainhash"
    21  	"github.com/decred/dcrd/dcrutil/v4"
    22  	chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4"
    23  	"github.com/decred/dcrd/rpcclient/v8"
    24  	"github.com/decred/dcrd/wire"
    25  )
    26  
    27  const (
    28  	ChainID = 42
    29  )
    30  
    31  var (
    32  	compatibleNodeRPCVersions = []dex.Semver{
    33  		{Major: 8, Minor: 0, Patch: 0}, // 1.8-pre, just dropped unused ticket RPCs
    34  		{Major: 7, Minor: 0, Patch: 0}, // 1.7 release, new gettxout args
    35  	}
    36  )
    37  
    38  func init() {
    39  	chain.RegisterChainConstructor(42, NewDecred)
    40  }
    41  
    42  type DecredConfigFile struct {
    43  	RPCUser   string `json:"rpcuser"`
    44  	RPCPass   string `json:"rpcpass"`
    45  	RPCListen string `json:"rpclisten"`
    46  	RPCCert   string `json:"rpccert"`
    47  	NodeRelay string `json:"nodeRelay"`
    48  }
    49  
    50  type decredChain struct {
    51  	cfg  *DecredConfigFile
    52  	net  dex.Network
    53  	log  dex.Logger
    54  	fees chan uint64
    55  
    56  	cl        *rpcclient.Client
    57  	connected atomic.Bool
    58  }
    59  
    60  func NewDecred(rawConfig json.RawMessage, log dex.Logger, net dex.Network) (chain.Chain, error) {
    61  	var cfg DecredConfigFile
    62  	if err := json.Unmarshal(rawConfig, &cfg); err != nil {
    63  		return nil, fmt.Errorf("error parsing configuration: %w", err)
    64  	}
    65  	return &decredChain{
    66  		cfg:  &cfg,
    67  		net:  net,
    68  		log:  log,
    69  		fees: make(chan uint64, 1),
    70  	}, nil
    71  }
    72  
    73  func (c *decredChain) Connect(ctx context.Context) (_ *sync.WaitGroup, err error) {
    74  	cfg := c.cfg
    75  	if cfg.NodeRelay == "" {
    76  		c.cl, err = connectNodeRPC(cfg.RPCListen, cfg.RPCUser, cfg.RPCPass, cfg.RPCCert)
    77  	} else {
    78  		c.cl, err = connectLocalHTTP(cfg.NodeRelay, cfg.RPCUser, cfg.RPCPass)
    79  	}
    80  	if err != nil {
    81  		return nil, fmt.Errorf("error connecting RPC client: %w", err)
    82  	}
    83  
    84  	if err = c.initialize(ctx); err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	c.connected.Store(true)
    89  	defer c.connected.Store(false)
    90  
    91  	var wg sync.WaitGroup
    92  	wg.Add(1)
    93  	go func() {
    94  		defer wg.Done()
    95  		c.monitorFees(ctx)
    96  	}()
    97  
    98  	return &wg, nil
    99  }
   100  
   101  func (c *decredChain) initialize(ctx context.Context) error {
   102  	net, err := c.cl.GetCurrentNet(ctx)
   103  	if err != nil {
   104  		return fmt.Errorf("getcurrentnet failure: %w", err)
   105  	}
   106  	var wantCurrencyNet wire.CurrencyNet
   107  	switch c.net {
   108  	case dex.Testnet:
   109  		wantCurrencyNet = wire.TestNet3
   110  	case dex.Mainnet:
   111  		wantCurrencyNet = wire.MainNet
   112  	case dex.Regtest: // dex.Simnet
   113  		wantCurrencyNet = wire.SimNet
   114  	}
   115  	if net != wantCurrencyNet {
   116  		return fmt.Errorf("wrong net %v", net.String())
   117  	}
   118  
   119  	// Check the required API versions.
   120  	versions, err := c.cl.Version(ctx)
   121  	if err != nil {
   122  		return fmt.Errorf("DCR node version fetch error: %w", err)
   123  	}
   124  
   125  	ver, exists := versions["dcrdjsonrpcapi"]
   126  	if !exists {
   127  		return fmt.Errorf("dcrd.Version response missing 'dcrdjsonrpcapi'")
   128  	}
   129  	nodeSemver := dex.NewSemver(ver.Major, ver.Minor, ver.Patch)
   130  	if !dex.SemverCompatibleAny(compatibleNodeRPCVersions, nodeSemver) {
   131  		return fmt.Errorf("dcrd has an incompatible JSON-RPC version %s, require one of %s",
   132  			nodeSemver, compatibleNodeRPCVersions)
   133  	}
   134  
   135  	// Verify dcrd has tx index enabled (required for getrawtransaction).
   136  	info, err := c.cl.GetInfo(ctx)
   137  	if err != nil {
   138  		return fmt.Errorf("dcrd getinfo check failed: %w", err)
   139  	}
   140  	if !info.TxIndex {
   141  		return errors.New("dcrd does not have transaction index enabled (specify --txindex)")
   142  	}
   143  
   144  	// Prime the cache with the best block.
   145  	tip, err := c.cl.GetBestBlockHash(ctx)
   146  	if err != nil {
   147  		return fmt.Errorf("error getting best block from dcrd: %w", err)
   148  	}
   149  	if tip == nil {
   150  		return fmt.Errorf("nil best block hash?")
   151  	}
   152  	// if bestHash != nil {
   153  	// 	_, err := dcr.getDcrBlock(ctx, bestHash)
   154  	// 	if err != nil {
   155  	// 		return nil, fmt.Errorf("error priming the cache: %w", err)
   156  	// 	}
   157  	// }
   158  
   159  	// if _, err = c.cl.FeeRate(ctx); err != nil {
   160  	// 	c.log.Warnf("Decred backend started without fee estimation available: %v", err)
   161  	// }
   162  	return nil
   163  }
   164  
   165  type query struct {
   166  	Method string            `json:"method"`
   167  	Args   []json.RawMessage `json:"args"`
   168  }
   169  
   170  // func (c *decredChain) Query(ctx context.Context, rawQuery chain.Query) (chain.Result, error) {
   171  // 	var q query
   172  // 	if err := json.Unmarshal(rawQuery, &q); err != nil {
   173  // 		return nil, chain.BadQueryError(fmt.Errorf("error parsing raw query: %w", err))
   174  // 	}
   175  
   176  // 	if q.Method == "" {
   177  // 		return nil, chain.BadQueryError(errors.New("invalid query parameters. no method"))
   178  // 	}
   179  
   180  // 	res, err := c.cl.RawRequest(ctx, q.Method, q.Args)
   181  // 	if err != nil {
   182  // 		// Could potentially try to parse certain errors here
   183  
   184  // 		return nil, fmt.Errorf("error performing query: %w", err)
   185  // 	}
   186  
   187  // 	return chain.Result(res), nil
   188  // }
   189  
   190  func (c *decredChain) Connected() bool {
   191  	return c.connected.Load()
   192  }
   193  
   194  func (c *decredChain) FeeChannel() <-chan uint64 {
   195  	return c.fees
   196  }
   197  
   198  func (c *decredChain) monitorFees(ctx context.Context) {
   199  	tick := time.NewTicker(feeMonitorTick)
   200  	var tip *chainhash.Hash
   201  	for {
   202  		select {
   203  		case <-tick.C:
   204  		case <-ctx.Done():
   205  			return
   206  		}
   207  
   208  		newTip, err := c.cl.GetBestBlockHash(ctx)
   209  		if err != nil {
   210  			c.connected.Store(false)
   211  			c.log.Errorf("Decred is not connected: %w", err)
   212  			continue
   213  		}
   214  		if newTip == nil { // sanity check
   215  			c.log.Error("nil tip hash?")
   216  			continue
   217  		}
   218  		if tip != nil && *tip == *newTip {
   219  			continue
   220  		}
   221  		tip = newTip
   222  		c.connected.Store(true)
   223  		// estimatesmartfee 1 returns extremely high rates on DCR.
   224  		estimateFeeResult, err := c.cl.EstimateSmartFee(ctx, 2, chainjson.EstimateSmartFeeConservative)
   225  		if err != nil {
   226  			c.log.Errorf("estimatesmartfee error: %w", err)
   227  			continue
   228  		}
   229  		atomsPerKB, err := dcrutil.NewAmount(estimateFeeResult.FeeRate)
   230  		if err != nil {
   231  			c.log.Errorf("NewAmount error: %w", err)
   232  			continue
   233  		}
   234  		atomsPerB := uint64(math.Round(float64(atomsPerKB) / 1000))
   235  		if atomsPerB == 0 {
   236  			atomsPerB = 1
   237  		}
   238  		select {
   239  		case c.fees <- atomsPerB:
   240  		case <-time.After(time.Second * 5):
   241  			c.log.Errorf("fee channel is blocking")
   242  		}
   243  	}
   244  }
   245  
   246  func (c *decredChain) CheckBond(b *tanka.Bond) error {
   247  
   248  	// TODO: Validate bond
   249  
   250  	return nil
   251  }
   252  
   253  func (c *decredChain) AuditHTLC(*tanka.HTLCAudit) (bool, error) {
   254  
   255  	// TODO: Perform the audit
   256  
   257  	return true, nil
   258  }
   259  
   260  // connectNodeRPC attempts to create a new websocket connection to a dcrd node
   261  // with the given credentials and notification handlers.
   262  func connectNodeRPC(host, user, pass, cert string) (*rpcclient.Client, error) {
   263  	dcrdCerts, err := os.ReadFile(cert)
   264  	if err != nil {
   265  		return nil, fmt.Errorf("TLS certificate read error: %w", err)
   266  	}
   267  
   268  	config := &rpcclient.ConnConfig{
   269  		Host:         host,
   270  		Endpoint:     "ws", // websocket
   271  		User:         user,
   272  		Pass:         pass,
   273  		Certificates: dcrdCerts,
   274  	}
   275  
   276  	dcrdClient, err := rpcclient.New(config, nil)
   277  	if err != nil {
   278  		return nil, fmt.Errorf("failed to start dcrd RPC client: %w", err)
   279  	}
   280  
   281  	return dcrdClient, nil
   282  }
   283  
   284  func connectLocalHTTP(host, user, pass string) (*rpcclient.Client, error) {
   285  	config := &rpcclient.ConnConfig{
   286  		Host:         host,
   287  		HTTPPostMode: true,
   288  		DisableTLS:   true,
   289  		User:         user,
   290  		Pass:         pass,
   291  	}
   292  
   293  	dcrdClient, err := rpcclient.New(config, nil)
   294  	if err != nil {
   295  		return nil, fmt.Errorf("failed to start dcrd RPC client: %w", err)
   296  	}
   297  
   298  	return dcrdClient, nil
   299  }