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:  &ltc.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  }