decred.org/dcrdex@v1.0.5/client/cmd/mmbaltracker/main.go (about)

     1  package main
     2  
     3  /*
     4   * Starts a process that repeatedly calls the mmavailablebalances command to
     5   * to check for changes in the available balances for market making on the
     6   * specified markets. Whenever there is a diff, it is logged. This is used
     7   * to check for bugs in the balance tracking logic. If there is a diff without
     8   * a bot being started, stopped, or updated, and the wallet is not handling
     9   * bonds, then there is a bug.
    10   */
    11  
    12  import (
    13  	"context"
    14  	"encoding/json"
    15  	"fmt"
    16  	"os"
    17  	"os/exec"
    18  	"os/signal"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  
    23  	"decred.org/dcrdex/client/mm"
    24  	"decred.org/dcrdex/dex"
    25  )
    26  
    27  var (
    28  	log = dex.StdOutLogger("BALTRACKER", dex.LevelDebug)
    29  )
    30  
    31  func printUsage() {
    32  	fmt.Println("Usage: mmbaltracker <configpath> <market>")
    33  	fmt.Println("  <configpath> is the path to the market making configuration file.")
    34  	fmt.Println("  <market> is a market in the form <host>-<baseassetid>-<quoteassetid>. You can specify multiple markets.")
    35  }
    36  
    37  func parseMkt(mkt string) (*mm.MarketWithHost, error) {
    38  	parts := strings.Split(mkt, "-")
    39  	if len(parts) != 3 {
    40  		return nil, fmt.Errorf("invalid market format")
    41  	}
    42  
    43  	host := parts[0]
    44  	baseID, err := strconv.Atoi(parts[1])
    45  	if err != nil {
    46  		return nil, fmt.Errorf("invalid base asset ID")
    47  	}
    48  
    49  	quoteID, err := strconv.Atoi(parts[2])
    50  	if err != nil {
    51  		return nil, fmt.Errorf("invalid quote asset ID")
    52  	}
    53  
    54  	return &mm.MarketWithHost{
    55  		Host:    host,
    56  		BaseID:  uint32(baseID),
    57  		QuoteID: uint32(quoteID),
    58  	}, nil
    59  }
    60  
    61  type balances struct {
    62  	DEXBalances map[uint32]uint64 `json:"dexBalances"`
    63  	CEXBalances map[uint32]uint64 `json:"cexBalances"`
    64  }
    65  
    66  func getAvailableBalances(mkt *mm.MarketWithHost, configPath string) (bals *balances, err error) {
    67  	cmd := exec.Command("dexcctl", "mmavailablebalances", configPath,
    68  		mkt.Host, strconv.Itoa(int(mkt.BaseID)), strconv.Itoa(int(mkt.QuoteID)))
    69  	out, err := cmd.Output()
    70  	if err != nil {
    71  		return nil, fmt.Errorf("error getting available balances: %v", err)
    72  	}
    73  
    74  	bals = new(balances)
    75  	err = json.Unmarshal(out, bals)
    76  	if err != nil {
    77  		return nil, fmt.Errorf("error unmarshalling available balances: %v", err)
    78  	}
    79  
    80  	return bals, nil
    81  }
    82  
    83  func main() {
    84  	if len(os.Args) < 3 {
    85  		printUsage()
    86  		os.Exit(1)
    87  	}
    88  
    89  	configPath := os.Args[1]
    90  
    91  	currBalances := make(map[mm.MarketWithHost]*balances, len(os.Args)-2)
    92  	for i := 2; i < len(os.Args); i++ {
    93  		mkt, err := parseMkt(os.Args[i])
    94  		if err != nil {
    95  			log.Errorf("Error parsing market: %v\n", err)
    96  			os.Exit(1)
    97  		}
    98  
    99  		currBalances[*mkt], err = getAvailableBalances(mkt, configPath)
   100  		if err != nil {
   101  			log.Errorf("Error getting initial balances: %v\n", err)
   102  			os.Exit(1)
   103  		}
   104  	}
   105  
   106  	log.Infof("Initial Balances:")
   107  	for mkt, bals := range currBalances {
   108  		log.Infof("Market: %s-%d-%d", mkt.Host, mkt.BaseID, mkt.QuoteID)
   109  		log.Infof("  DEX Balances:")
   110  		for assetID, bal := range bals.DEXBalances {
   111  			log.Infof("    %d: %d", assetID, bal)
   112  		}
   113  		log.Infof("  CEX Balances:")
   114  		for assetID, bal := range bals.CEXBalances {
   115  			log.Infof("    %d: %d", assetID, bal)
   116  		}
   117  	}
   118  
   119  	type diff struct {
   120  		assetID uint32
   121  		oldBal  uint64
   122  		newBal  uint64
   123  	}
   124  
   125  	checkForDiffs := func(mkt *mm.MarketWithHost) {
   126  		newBals, err := getAvailableBalances(mkt, configPath)
   127  		if err != nil {
   128  			log.Errorf("Error getting balances: %v\n", err)
   129  			return
   130  		}
   131  
   132  		dexDiffs := make([]*diff, 0)
   133  		cexDiffs := make([]*diff, 0)
   134  
   135  		for assetID, newBal := range newBals.DEXBalances {
   136  			oldBal := currBalances[*mkt].DEXBalances[assetID]
   137  			if oldBal != newBal {
   138  				dexDiffs = append(dexDiffs, &diff{assetID, oldBal, newBal})
   139  			}
   140  			currBalances[*mkt].DEXBalances[assetID] = newBal
   141  		}
   142  		for assetID, newBal := range newBals.CEXBalances {
   143  			oldBal := currBalances[*mkt].CEXBalances[assetID]
   144  			if oldBal != newBal {
   145  				cexDiffs = append(cexDiffs, &diff{assetID, oldBal, newBal})
   146  			}
   147  			currBalances[*mkt].CEXBalances[assetID] = newBal
   148  		}
   149  
   150  		logStr := ""
   151  
   152  		if len(dexDiffs) > 0 || len(cexDiffs) > 0 {
   153  			logStr += "================================================\n"
   154  			logStr += fmt.Sprintf("\nDiffs on Market: %s-%d-%d", mkt.Host, mkt.BaseID, mkt.QuoteID)
   155  			if len(dexDiffs) > 0 {
   156  				logStr += "\n  DEX diffs:"
   157  				for _, d := range dexDiffs {
   158  					logStr += fmt.Sprintf("\n    %s: %d -> %d (%d)", dex.BipIDSymbol(d.assetID), d.oldBal, d.newBal, int64(d.newBal)-int64(d.oldBal))
   159  				}
   160  			}
   161  
   162  			if len(cexDiffs) > 0 {
   163  				logStr += "\n  CEX diffs:"
   164  				for _, d := range cexDiffs {
   165  					logStr += fmt.Sprintf("\n    %s: %d -> %d (%d)", dex.BipIDSymbol(d.assetID), d.oldBal, d.newBal, int64(d.newBal)-int64(d.oldBal))
   166  				}
   167  			}
   168  			logStr += "\n\n"
   169  			log.Infof(logStr)
   170  		}
   171  	}
   172  
   173  	ctx, cancel := context.WithCancel(context.Background())
   174  
   175  	sigChan := make(chan os.Signal, 1)
   176  	signal.Notify(sigChan, os.Interrupt)
   177  	go func() {
   178  		<-sigChan
   179  		cancel()
   180  	}()
   181  
   182  	timer := time.NewTicker(time.Second * 2)
   183  	for {
   184  		select {
   185  		case <-timer.C:
   186  			for mkt := range currBalances {
   187  				checkForDiffs(&mkt)
   188  			}
   189  		case <-ctx.Done():
   190  			log.Infof("Exiting...")
   191  			os.Exit(0)
   192  		}
   193  	}
   194  }