decred.org/dcrdex@v1.0.5/server/cmd/usermatches/main.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 main 5 6 import ( 7 "context" 8 "encoding/csv" 9 "encoding/hex" 10 "flag" 11 "fmt" 12 "os" 13 "os/signal" 14 "strconv" 15 "strings" 16 17 "decred.org/dcrdex/dex" 18 "decred.org/dcrdex/dex/calc" 19 "decred.org/dcrdex/server/account" 20 "decred.org/dcrdex/server/asset" 21 "decred.org/dcrdex/server/asset/btc" 22 "decred.org/dcrdex/server/asset/dcr" 23 "decred.org/dcrdex/server/asset/ltc" 24 "decred.org/dcrdex/server/db" 25 "decred.org/dcrdex/server/db/driver/pg" 26 ) 27 28 // We do not need a Backend Setup, just the Drivers to call DecodeCoinID. While 29 // we can do this with asset.DecodeCoinID(assetID, coinID), doing the following 30 // drastically reduces the locking/unlocking (asset.driversMtx) that would be 31 // required to decode coin IDs, and we will likely be doing many. 32 var assets = map[uint32]asset.Driver{ 33 0: &btc.Driver{}, 34 2: <c.Driver{}, 35 42: &dcr.Driver{}, 36 } 37 38 var dbhost = flag.String("host", "/run/postgresql", "pg host") // default to unix socket, but 127.0.0.1 would be common too 39 var dbuser = flag.String("user", "dcrdex", "db username") 40 var dbpass = flag.String("pass", "", "db password") 41 var dbname = flag.String("dbname", "dcrdex", "db name") 42 var dbport = flag.Int("port", 5432, "db port") 43 var base = flag.Uint("base", 42, "market base asset id") 44 var quote = flag.Uint("quote", 0, "market quote asset id") 45 var acct = flag.String("acct", "", "filter for dex account") // default is all accounts 46 47 // MatchData supplements a db.MatchData with decoded swap and redeem coins. 48 type MatchData struct { 49 db.MatchData 50 MakerSwap string 51 TakerSwap string 52 MakerRedeem string 53 TakerRedeem string 54 } 55 56 func convertMatchData(baseAsset, quoteAsset asset.Driver, md *db.MatchDataWithCoins) *MatchData { 57 matchData := MatchData{ 58 MatchData: md.MatchData, 59 } 60 // asset0 is the maker swap / taker redeem asset. 61 // asset1 is the taker swap / maker redeem asset. 62 // Maker selling means asset 0 is base; asset 1 is quote. 63 asset0, asset1 := baseAsset, quoteAsset 64 if md.TakerSell { 65 asset0, asset1 = quoteAsset, baseAsset 66 } 67 if len(md.MakerSwapCoin) > 0 { 68 coinStr, err := asset0.DecodeCoinID(md.MakerSwapCoin) 69 if err != nil { 70 fmt.Printf("Unable to decode coin %x: %v\n", md.MakerSwapCoin, err) 71 // leave empty and keep chugging 72 } 73 matchData.MakerSwap = coinStr 74 } 75 if len(md.TakerSwapCoin) > 0 { 76 coinStr, err := asset1.DecodeCoinID(md.TakerSwapCoin) 77 if err != nil { 78 fmt.Printf("Unable to decode coin %x: %v\n", md.TakerSwapCoin, err) 79 } 80 matchData.TakerSwap = coinStr 81 } 82 if len(md.MakerRedeemCoin) > 0 { 83 coinStr, err := asset0.DecodeCoinID(md.MakerRedeemCoin) 84 if err != nil { 85 fmt.Printf("Unable to decode coin %x: %v\n", md.MakerRedeemCoin, err) 86 } 87 matchData.MakerRedeem = coinStr 88 } 89 if len(md.TakerRedeemCoin) > 0 { 90 coinStr, err := asset1.DecodeCoinID(md.TakerRedeemCoin) 91 if err != nil { 92 fmt.Printf("Unable to decode coin %x: %v\n", md.TakerRedeemCoin, err) 93 } 94 matchData.TakerRedeem = coinStr 95 } 96 97 return &matchData 98 } 99 100 // MarketMatchesStreaming streams all matches for market with base and quote 101 // through a MatchData processing function, which is a wrapper around the 102 // provided function and convertMatchData. The provided function should do two 103 // main things: (1) apply some filtering, and (2) write the match data out 104 // somewhere, which in this app is a CSV file. 105 func MarketMatchesStreaming(storage db.DEXArchivist, base, quote uint32, includeInactive bool, N int64, f func(*MatchData) error) (int, error) { 106 baseAsset := assets[base] 107 if baseAsset == nil { 108 return 0, fmt.Errorf("asset %d not found", base) 109 } 110 quoteAsset := assets[quote] 111 if quoteAsset == nil { 112 return 0, fmt.Errorf("asset %d not found", quote) 113 } 114 fDB := func(md *db.MatchDataWithCoins) error { 115 matchData := convertMatchData(baseAsset, quoteAsset, md) 116 return f(matchData) 117 } 118 return storage.MarketMatchesStreaming(base, quote, includeInactive, N, fDB) 119 } 120 121 func main() { 122 if err := mainCore(); err != nil { 123 fmt.Println(err) 124 os.Exit(1) 125 } 126 os.Exit(0) 127 } 128 129 func mainCore() error { 130 ctx, quit := context.WithCancel(context.Background()) 131 defer quit() 132 killChan := make(chan os.Signal, 1) 133 signal.Notify(killChan, os.Interrupt) 134 go func() { 135 <-killChan 136 quit() 137 fmt.Println("Shutting down...") 138 }() 139 140 flag.Parse() 141 142 base, quote := uint32(*base), uint32(*quote) 143 name, err := dex.MarketName(base, quote) 144 if err != nil { 145 return err 146 } 147 mkt := &dex.MarketInfo{ 148 Name: name, 149 Base: base, 150 Quote: quote, 151 } 152 153 baseAsset := assets[base] 154 if baseAsset == nil { 155 return fmt.Errorf("asset %d not found", base) 156 } 157 baseUnitInfo := baseAsset.UnitInfo() 158 quoteAsset := assets[quote] 159 if quoteAsset == nil { 160 return fmt.Errorf("asset %d not found", quote) 161 } 162 quoteUnitInfo := quoteAsset.UnitInfo() 163 164 pgCfg := &pg.Config{ 165 Host: *dbhost, 166 Port: strconv.Itoa(*dbport), 167 User: *dbuser, 168 Pass: *dbpass, 169 DBName: *dbname, 170 MarketCfg: []*dex.MarketInfo{mkt}, 171 } 172 archiver, err := pg.NewArchiverForRead(ctx, pgCfg) 173 if err != nil { 174 return err 175 } 176 defer archiver.Close() 177 178 var acctID account.AccountID 179 var haveAccount bool 180 switch len(*acct) { 181 case 0: // no account filter 182 case account.HashSize * 2: 183 acctB, err := hex.DecodeString(*acct) 184 if err != nil { 185 return err 186 } 187 copy(acctID[:], acctB) 188 haveAccount = true 189 default: 190 return fmt.Errorf("bad acct ID %v", *acct) 191 } 192 193 csvfile, err := os.Create(fmt.Sprintf("acct_matches_%v.csv", acctID)) 194 if err != nil { 195 return fmt.Errorf("error creating csv file: %w", err) 196 } 197 defer csvfile.Close() 198 199 csvwriter := csv.NewWriter(csvfile) 200 defer csvwriter.Flush() 201 202 err = csvwriter.Write([]string{"unixtime", "maker", "taker", "quantity", "rate", 203 "isTakerSell", "makerSwapTx", "makerSwapVout", "makerRedeemTx", "makerRedeemVin", 204 "takerSwapTx", "takerSwapVout", "takerRedeemTx", "takerRedeemVin"}) 205 if err != nil { 206 return fmt.Errorf("ERROR: csvwriter.Write failed: %w", err) 207 } 208 209 splitTx := func(txinout string) (tx, vinout string, err error) { 210 if txinout == "" { 211 return // ok 212 } 213 txsplit := strings.Split(txinout, ":") 214 if len(txsplit) != 2 { 215 err = fmt.Errorf("txinout (%s) not formatted as a txin/out", txinout) 216 return 217 } 218 _, err = strconv.ParseUint(txsplit[1], 10, 32) 219 if err != nil { 220 err = fmt.Errorf("strconv.ParseUint(%s): %w", txsplit[1], err) 221 return 222 } 223 return txsplit[0], txsplit[1], nil 224 } 225 226 _, err = MarketMatchesStreaming(archiver, base, quote, true, -1, func(md *MatchData) error { 227 if err := ctx.Err(); err != nil { 228 return err 229 } 230 if haveAccount && (md.MakerAcct != acctID && md.TakerAcct != acctID) { 231 return nil 232 } 233 234 makerSwapTx, makerSwapVout, err := splitTx(md.MakerSwap) 235 if err != nil { 236 return fmt.Errorf("strings.Split(%s): %w", md.MakerSwap, err) 237 } 238 makerRedeemTx, makerRedeemVin, err := splitTx(md.MakerRedeem) 239 if err != nil { 240 return fmt.Errorf("strings.Split(%s): %w", md.MakerRedeem, err) 241 } 242 takerSwapTx, takerSwapVout, err := splitTx(md.TakerSwap) 243 if err != nil { 244 return fmt.Errorf("strings.Split(%s): %w", md.TakerSwap, err) 245 } 246 takerRedeemTx, takerRedeemVin, err := splitTx(md.TakerRedeem) 247 if err != nil { 248 return fmt.Errorf("strings.Split(%s): %w", md.TakerRedeem, err) 249 } 250 251 err = csvwriter.Write([]string{ 252 strconv.FormatUint(md.Epoch.Idx*md.Epoch.Dur/1000, 10), 253 md.MakerAcct.String(), 254 md.TakerAcct.String(), 255 baseUnitInfo.ConventionalString(md.Quantity), 256 strconv.FormatFloat(calc.ConventionalRate(md.Rate, baseUnitInfo, quoteUnitInfo), 'f', -1, 64), 257 strconv.FormatBool(md.TakerSell), 258 makerSwapTx, makerSwapVout, makerRedeemTx, makerRedeemVin, 259 takerSwapTx, takerSwapVout, takerRedeemTx, takerRedeemVin, 260 }) 261 if err != nil { 262 return fmt.Errorf("csvwriter.Write: %w", err) 263 } 264 return nil 265 }) 266 267 return err 268 }