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 }