decred.org/dcrdex@v1.0.5/server/db/driver/pg/matches_online_test.go (about)

     1  //go:build pgonline
     2  
     3  package pg
     4  
     5  import (
     6  	"bytes"
     7  	"errors"
     8  	"fmt"
     9  	"testing"
    10  	"time"
    11  
    12  	"decred.org/dcrdex/dex/candles"
    13  	"decred.org/dcrdex/dex/encode"
    14  	"decred.org/dcrdex/dex/order"
    15  	"decred.org/dcrdex/server/account"
    16  	"decred.org/dcrdex/server/db"
    17  )
    18  
    19  func TestInsertMatch(t *testing.T) {
    20  	if err := cleanTables(archie.db); err != nil {
    21  		t.Fatalf("cleanTables: %v", err)
    22  	}
    23  
    24  	// Make a perfect 1 lot match.
    25  	limitBuyStanding := newLimitOrder(false, 4500000, 1, order.StandingTiF, 0)
    26  	limitSellImmediate := newLimitOrder(true, 4490000, 1, order.ImmediateTiF, 10)
    27  
    28  	epochID := order.EpochID{132412341, 1000}
    29  	// Taker is selling.
    30  	matchA := newMatch(limitBuyStanding, limitSellImmediate, limitSellImmediate.Quantity, epochID)
    31  
    32  	base, quote := limitBuyStanding.Base(), limitBuyStanding.Quote()
    33  
    34  	matchAUpdated := matchA
    35  	matchAUpdated.Status = order.MakerSwapCast
    36  	// matchAUpdated.Sigs.MakerMatch = randomBytes(73)
    37  
    38  	cancelLOBuy := newCancelOrder(limitBuyStanding.ID(), base, quote, 0)
    39  	matchCancel := newMatch(limitBuyStanding, cancelLOBuy, 0, epochID)
    40  	matchCancel.Status = order.MatchComplete // will be forced to complete on store too
    41  
    42  	tests := []struct {
    43  		name     string
    44  		match    *order.Match
    45  		wantErr  bool
    46  		isCancel bool
    47  	}{
    48  		{
    49  			"store ok",
    50  			matchA,
    51  			false,
    52  			false,
    53  		},
    54  		{
    55  			"update ok",
    56  			matchAUpdated,
    57  			false,
    58  			false,
    59  		},
    60  		{
    61  			"update again ok",
    62  			matchAUpdated,
    63  			false,
    64  			false,
    65  		},
    66  		{
    67  			"cancel",
    68  			matchCancel,
    69  			false,
    70  			true,
    71  		},
    72  	}
    73  
    74  	for _, tt := range tests {
    75  		t.Run(tt.name, func(t *testing.T) {
    76  			err := archie.InsertMatch(tt.match)
    77  			if (err != nil) != tt.wantErr {
    78  				t.Errorf("InsertMatch() error = %v, wantErr %v", err, tt.wantErr)
    79  			}
    80  
    81  			if tt.wantErr {
    82  				return
    83  			}
    84  
    85  			matchID := tt.match.ID()
    86  			matchData, err := archie.MatchByID(matchID, base, quote)
    87  			if err != nil {
    88  				t.Fatal(err)
    89  			}
    90  			if matchData.ID != matchID {
    91  				t.Errorf("Retrieved match with ID %v, expected %v", matchData.ID, matchID)
    92  			}
    93  			if matchData.Status != tt.match.Status {
    94  				t.Errorf("Incorrect match status, got %d, expected %d",
    95  					matchData.Status, tt.match.Status)
    96  			}
    97  			if tt.isCancel {
    98  				if matchData.Active {
    99  					t.Errorf("Incorrect match active flag, got %v, expected false",
   100  						matchData.Active)
   101  				}
   102  				trade := tt.match.Taker.Trade()
   103  				if trade != nil {
   104  					if matchData.TakerSell != trade.Sell {
   105  						t.Errorf("expected takerSell = %v, got %v", trade.Sell, matchData.TakerSell)
   106  					}
   107  					if matchData.BaseRate != tt.match.FeeRateBase {
   108  						t.Errorf("expected base fee rate %d, got %d", tt.match.FeeRateBase, matchData.BaseRate)
   109  					}
   110  				} else {
   111  					if matchData.BaseRate != 0 {
   112  						t.Errorf("cancel order should have 0 base fee rate, got %d", matchData.BaseRate)
   113  					}
   114  					if matchData.QuoteRate != 0 {
   115  						t.Errorf("cancel order should have 0 quote fee rate, got %d", matchData.QuoteRate)
   116  					}
   117  					if matchData.TakerSell {
   118  						t.Errorf("cancel order should have false for takerSell")
   119  					}
   120  				}
   121  				if matchData.TakerAddr != "" {
   122  					t.Errorf("Expected empty taker address for cancel match, got %v", matchData.TakerAddr)
   123  				}
   124  				if matchData.MakerAddr != "" {
   125  					t.Errorf("Expected empty maker address for cancel match, got %v", matchData.MakerAddr)
   126  				}
   127  			}
   128  		})
   129  	}
   130  }
   131  
   132  func TestSetSwapData(t *testing.T) {
   133  	if err := cleanTables(archie.db); err != nil {
   134  		t.Fatalf("cleanTables: %v", err)
   135  	}
   136  
   137  	// Make a perfect 1 lot match.
   138  	limitBuyStanding := newLimitOrder(false, 4500000, 1, order.StandingTiF, 0)
   139  	limitSellImmediate := newLimitOrder(true, 4490000, 1, order.ImmediateTiF, 10)
   140  
   141  	epochID := order.EpochID{132412341, 1000}
   142  	matchA := newMatch(limitBuyStanding, limitSellImmediate, limitSellImmediate.Quantity, epochID)
   143  	matchID := matchA.ID()
   144  
   145  	base, quote := limitBuyStanding.Base(), limitBuyStanding.Quote()
   146  
   147  	checkMatch := func(wantStatus order.MatchStatus, wantActive bool) error {
   148  		matchData, err := archie.MatchByID(matchID, base, quote)
   149  		if err != nil {
   150  			return err
   151  		}
   152  		if matchData.ID != matchID {
   153  			return fmt.Errorf("Retrieved match with ID %v, expected %v", matchData.ID, matchID)
   154  		}
   155  		if matchData.Status != wantStatus {
   156  			return fmt.Errorf("Incorrect match status, got %d, expected %d",
   157  				matchData.Status, wantStatus)
   158  		}
   159  		if matchData.Active != wantActive {
   160  			return fmt.Errorf("Incorrect match active flag, got %v, expected %v",
   161  				matchData.Active, wantActive)
   162  		}
   163  		return nil
   164  	}
   165  
   166  	err := archie.InsertMatch(matchA)
   167  	if err != nil {
   168  		t.Errorf("InsertMatch() failed: %v", err)
   169  	}
   170  
   171  	if err = checkMatch(order.NewlyMatched, true); err != nil {
   172  		t.Fatal(err)
   173  	}
   174  
   175  	mid := db.MarketMatchID{
   176  		MatchID: matchA.ID(),
   177  		Base:    base,
   178  		Quote:   quote,
   179  	}
   180  
   181  	// Match Ack Sig A (maker's match ack sig)
   182  	sigMakerMatch := randomBytes(73)
   183  	err = archie.SaveMatchAckSigA(mid, sigMakerMatch)
   184  	if err != nil {
   185  		t.Fatal(err)
   186  	}
   187  	status, swapData, err := archie.SwapData(mid)
   188  	if err != nil {
   189  		t.Fatal(err)
   190  	}
   191  	if status != order.NewlyMatched {
   192  		t.Errorf("Got status %v, expected %v", status, order.NewlyMatched)
   193  	}
   194  	if !bytes.Equal(swapData.SigMatchAckMaker, sigMakerMatch) {
   195  		t.Fatalf("SigMatchAckMaker incorrect. got %v, expected %v",
   196  			swapData.SigMatchAckMaker, sigMakerMatch)
   197  	}
   198  
   199  	// Match Ack Sig B (taker's match ack sig)
   200  	sigTakerMatch := randomBytes(73)
   201  	err = archie.SaveMatchAckSigB(mid, sigTakerMatch)
   202  	if err != nil {
   203  		t.Fatal(err)
   204  	}
   205  	status, swapData, err = archie.SwapData(mid)
   206  	if err != nil {
   207  		t.Fatal(err)
   208  	}
   209  	if status != order.NewlyMatched {
   210  		t.Errorf("Got status %v, expected %v", status, order.NewlyMatched)
   211  	}
   212  	if !bytes.Equal(swapData.SigMatchAckTaker, sigTakerMatch) {
   213  		t.Fatalf("SigMatchAckTaker incorrect. got %v, expected %v",
   214  			swapData.SigMatchAckTaker, sigTakerMatch)
   215  	}
   216  
   217  	// Contract A
   218  	contractA := randomBytes(128)
   219  	coinIDA := randomBytes(36)
   220  	contractATime := int64(1234)
   221  	err = archie.SaveContractA(mid, contractA, coinIDA, contractATime)
   222  	if err != nil {
   223  		t.Fatal(err)
   224  	}
   225  
   226  	status, swapData, err = archie.SwapData(mid)
   227  	if err != nil {
   228  		t.Fatal(err)
   229  	}
   230  	if status != order.MakerSwapCast {
   231  		t.Errorf("Got status %v, expected %v", status, order.MakerSwapCast)
   232  	}
   233  	if !bytes.Equal(swapData.ContractA, contractA) {
   234  		t.Fatalf("ContractA incorrect. got %v, expected %v",
   235  			swapData.ContractA, contractA)
   236  	}
   237  	if !bytes.Equal(swapData.ContractACoinID, coinIDA) {
   238  		t.Fatalf("ContractACoinID incorrect. got %v, expected %v",
   239  			swapData.ContractACoinID, coinIDA)
   240  	}
   241  	if swapData.ContractATime != contractATime {
   242  		t.Fatalf("ContractATime incorrect. got %d, expected %d",
   243  			swapData.ContractATime, contractATime)
   244  	}
   245  
   246  	// Party B's signature for acknowledgement of contract A
   247  	auditSigB := randomBytes(73)
   248  	if err = archie.SaveAuditAckSigB(mid, auditSigB); err != nil {
   249  		t.Fatal(err)
   250  	}
   251  
   252  	status, swapData, err = archie.SwapData(mid)
   253  	if err != nil {
   254  		t.Fatal(err)
   255  	}
   256  	if status != order.MakerSwapCast {
   257  		t.Errorf("Got status %v, expected %v", status, order.MakerSwapCast)
   258  	}
   259  	if !bytes.Equal(swapData.ContractAAckSig, auditSigB) {
   260  		t.Fatalf("ContractAAckSig incorrect. got %v, expected %v",
   261  			swapData.ContractAAckSig, auditSigB)
   262  	}
   263  
   264  	// Contract B
   265  	contractB := randomBytes(128)
   266  	coinIDB := randomBytes(36)
   267  	contractBTime := int64(1235)
   268  	err = archie.SaveContractB(mid, contractB, coinIDB, contractBTime)
   269  	if err != nil {
   270  		t.Fatal(err)
   271  	}
   272  
   273  	status, swapData, err = archie.SwapData(mid)
   274  	if err != nil {
   275  		t.Fatal(err)
   276  	}
   277  	if status != order.TakerSwapCast {
   278  		t.Errorf("Got status %v, expected %v", status, order.TakerSwapCast)
   279  	}
   280  	if !bytes.Equal(swapData.ContractB, contractB) {
   281  		t.Fatalf("ContractB incorrect. got %v, expected %v",
   282  			swapData.ContractB, contractB)
   283  	}
   284  	if !bytes.Equal(swapData.ContractBCoinID, coinIDB) {
   285  		t.Fatalf("ContractBCoinID incorrect. got %v, expected %v",
   286  			swapData.ContractBCoinID, coinIDB)
   287  	}
   288  	if swapData.ContractBTime != contractBTime {
   289  		t.Fatalf("ContractBTime incorrect. got %d, expected %d",
   290  			swapData.ContractBTime, contractBTime)
   291  	}
   292  
   293  	// Party A's signature for acknowledgement of contract B
   294  	auditSigA := randomBytes(73)
   295  	if err = archie.SaveAuditAckSigA(mid, auditSigA); err != nil {
   296  		t.Fatal(err)
   297  	}
   298  
   299  	status, swapData, err = archie.SwapData(mid)
   300  	if err != nil {
   301  		t.Fatal(err)
   302  	}
   303  	if status != order.TakerSwapCast {
   304  		t.Errorf("Got status %v, expected %v", status, order.TakerSwapCast)
   305  	}
   306  	if !bytes.Equal(swapData.ContractBAckSig, auditSigA) {
   307  		t.Fatalf("ContractBAckSig incorrect. got %v, expected %v",
   308  			swapData.ContractBAckSig, auditSigB)
   309  	}
   310  
   311  	// Redeem A
   312  	redeemCoinIDA := randomBytes(36)
   313  	secret := randomBytes(72)
   314  	redeemATime := int64(1234)
   315  	err = archie.SaveRedeemA(mid, redeemCoinIDA, secret, redeemATime)
   316  	if err != nil {
   317  		t.Fatal(err)
   318  	}
   319  	status, swapData, err = archie.SwapData(mid)
   320  	if err != nil {
   321  		t.Fatal(err)
   322  	}
   323  	if status != order.MakerRedeemed {
   324  		t.Errorf("Got status %v, expected %v", status, order.MakerRedeemed)
   325  	}
   326  	if !bytes.Equal(swapData.RedeemACoinID, redeemCoinIDA) {
   327  		t.Fatalf("RedeemACoinID incorrect. got %v, expected %v",
   328  			swapData.RedeemACoinID, redeemCoinIDA)
   329  	}
   330  	if !bytes.Equal(swapData.RedeemASecret, secret) {
   331  		t.Fatalf("RedeemASecret incorrect. got %v, expected %v",
   332  			swapData.RedeemASecret, secret)
   333  	}
   334  	if swapData.RedeemATime != redeemATime {
   335  		t.Fatalf("RedeemATime incorrect. got %d, expected %d",
   336  			swapData.RedeemATime, redeemATime)
   337  	}
   338  
   339  	// Party B's signature for acknowledgement of A's redemption
   340  	redeemAckSigB := randomBytes(73)
   341  	if err = archie.SaveRedeemAckSigB(mid, redeemAckSigB); err != nil {
   342  		t.Fatal(err)
   343  	}
   344  
   345  	status, swapData, err = archie.SwapData(mid)
   346  	if err != nil {
   347  		t.Fatal(err)
   348  	}
   349  	if status != order.MakerRedeemed {
   350  		t.Errorf("Got status %v, expected %v", status, order.MakerRedeemed)
   351  	}
   352  	if !bytes.Equal(swapData.RedeemAAckSig, redeemAckSigB) {
   353  		t.Fatalf("RedeemAAckSig incorrect. got %v, expected %v",
   354  			swapData.RedeemAAckSig, redeemAckSigB)
   355  	}
   356  
   357  	// Redeem B
   358  	redeemCoinIDB := randomBytes(36)
   359  	redeemBTime := int64(1234)
   360  	err = archie.SaveRedeemB(mid, redeemCoinIDB, redeemBTime)
   361  	if err != nil {
   362  		t.Fatal(err)
   363  	}
   364  
   365  	status, swapData, err = archie.SwapData(mid)
   366  	if err != nil {
   367  		t.Fatal(err)
   368  	}
   369  	if status != order.MatchComplete {
   370  		t.Errorf("Got status %v, expected %v", status, order.MatchComplete)
   371  	}
   372  	if !bytes.Equal(swapData.RedeemBCoinID, redeemCoinIDB) {
   373  		t.Fatalf("RedeemBCoinID incorrect. got %v, expected %v",
   374  			swapData.RedeemBCoinID, redeemCoinIDB)
   375  	}
   376  	if swapData.RedeemBTime != redeemBTime {
   377  		t.Fatalf("RedeemBTime incorrect. got %d, expected %d",
   378  			swapData.RedeemBTime, redeemBTime)
   379  	}
   380  
   381  	// Check active flag via MatchByID.
   382  	if err = checkMatch(order.MatchComplete, false); err != nil {
   383  		t.Fatal(err)
   384  	}
   385  }
   386  
   387  func TestMatchByID(t *testing.T) {
   388  	if err := cleanTables(archie.db); err != nil {
   389  		t.Fatalf("cleanTables: %v", err)
   390  	}
   391  
   392  	// Make a perfect 1 lot match.
   393  	limitBuyStanding := newLimitOrder(false, 4500000, 1, order.StandingTiF, 0)
   394  	limitSellImmediate := newLimitOrder(true, 4490000, 1, order.ImmediateTiF, 10)
   395  
   396  	base, quote := limitBuyStanding.Base(), limitBuyStanding.Quote()
   397  
   398  	// Store it.
   399  	epochID := order.EpochID{132412341, 1000}
   400  	match := newMatch(limitBuyStanding, limitSellImmediate, limitSellImmediate.Quantity, epochID)
   401  	err := archie.InsertMatch(match)
   402  	if err != nil {
   403  		t.Fatalf("InsertMatch() failed: %v", err)
   404  	}
   405  
   406  	tests := []struct {
   407  		name        string
   408  		matchID     order.MatchID
   409  		base, quote uint32
   410  		wantedErr   error
   411  	}{
   412  		{
   413  			"ok",
   414  			match.ID(),
   415  			base, quote,
   416  			nil,
   417  		},
   418  		{
   419  			"no order",
   420  			order.MatchID{},
   421  			base, quote,
   422  			db.ArchiveError{Code: db.ErrUnknownMatch},
   423  		},
   424  		{
   425  			"bad market",
   426  			match.ID(),
   427  			base, base,
   428  			db.ArchiveError{Code: db.ErrUnsupportedMarket},
   429  		},
   430  	}
   431  
   432  	for _, tt := range tests {
   433  		t.Run(tt.name, func(t *testing.T) {
   434  			matchData, err := archie.MatchByID(tt.matchID, tt.base, tt.quote)
   435  			if !db.SameErrorTypes(err, tt.wantedErr) {
   436  				t.Fatal(err)
   437  			}
   438  			if err == nil && matchData.ID != tt.matchID {
   439  				t.Errorf("Retrieved match with ID %v, expected %v", matchData.ID, tt.matchID)
   440  			}
   441  		})
   442  	}
   443  }
   444  
   445  func TestUserMatches(t *testing.T) {
   446  	if err := cleanTables(archie.db); err != nil {
   447  		t.Fatalf("cleanTables: %v", err)
   448  	}
   449  
   450  	// Make a perfect 1 lot match.
   451  	limitBuyStanding := newLimitOrder(false, 4500000, 1, order.StandingTiF, 0)
   452  	limitSellImmediate := newLimitOrder(true, 4490000, 1, order.ImmediateTiF, 10)
   453  
   454  	base, quote := limitBuyStanding.Base(), limitBuyStanding.Quote()
   455  
   456  	// Store it.
   457  	epochID := order.EpochID{132412341, 1000}
   458  	match := newMatch(limitBuyStanding, limitSellImmediate, limitSellImmediate.Quantity, epochID)
   459  	err := archie.InsertMatch(match)
   460  	if err != nil {
   461  		t.Fatalf("InsertMatch() failed: %v", err)
   462  	}
   463  
   464  	tests := []struct {
   465  		name        string
   466  		acctID      account.AccountID
   467  		numExpected int
   468  		wantedErr   error
   469  	}{
   470  		{
   471  			"ok maker",
   472  			limitBuyStanding.User(),
   473  			1,
   474  			nil,
   475  		},
   476  		{
   477  			"ok taker",
   478  			limitSellImmediate.User(),
   479  			1,
   480  			nil,
   481  		},
   482  		{
   483  			"nope",
   484  			randomAccountID(),
   485  			0,
   486  			nil,
   487  		},
   488  	}
   489  
   490  	for _, tt := range tests {
   491  		t.Run(tt.name, func(t *testing.T) {
   492  			matchData, err := archie.UserMatches(tt.acctID, base, quote)
   493  			if err != tt.wantedErr {
   494  				t.Fatal(err)
   495  			}
   496  			if len(matchData) != tt.numExpected {
   497  				t.Errorf("Retrieved %d matches for user %v, expected %d.", len(matchData), tt.acctID, tt.numExpected)
   498  			}
   499  		})
   500  	}
   501  }
   502  
   503  func TestMarketMatches(t *testing.T) {
   504  	if err := cleanTables(archie.db); err != nil {
   505  		t.Fatalf("cleanTables: %v", err)
   506  	}
   507  
   508  	// Make a perfect 1 lot match.
   509  	limitBuyStanding := newLimitOrder(false, 4500000, 1, order.StandingTiF, 0)
   510  	limitSellImmediate := newLimitOrder(true, 4490000, 1, order.ImmediateTiF, 10)
   511  
   512  	base, quote := limitBuyStanding.Base(), limitBuyStanding.Quote()
   513  
   514  	// Store it.
   515  	epochID := order.EpochID{132412341, 1000}
   516  	match := newMatch(limitBuyStanding, limitSellImmediate, limitSellImmediate.Quantity, epochID)
   517  	err := archie.InsertMatch(match)
   518  	if err != nil {
   519  		t.Fatalf("InsertMatch() failed: %v", err)
   520  	}
   521  	// Make another perfect 1 lot match.
   522  	limitBuyStanding = newLimitOrder(false, 4500000, 1, order.StandingTiF, 0)
   523  	limitSellImmediate = newLimitOrder(true, 4490000, 1, order.ImmediateTiF, 10)
   524  
   525  	// Store it.
   526  	match = newMatch(limitBuyStanding, limitSellImmediate, limitSellImmediate.Quantity, epochID)
   527  	err = archie.InsertMatch(match)
   528  	if err != nil {
   529  		t.Fatalf("InsertMatch() failed: %v", err)
   530  	}
   531  	archie.SetMatchInactive(db.MarketMatchID{
   532  		MatchID: match.ID(),
   533  		Base:    base,
   534  		Quote:   quote,
   535  	}, false)
   536  
   537  	// This one has txns.
   538  	mktMatchID := db.MarketMatchID{
   539  		MatchID: match.ID(),
   540  		Base:    limitBuyStanding.Base(),
   541  		Quote:   limitBuyStanding.Quote(),
   542  	}
   543  	midWithCoins := mktMatchID.MatchID
   544  	MakerSwap, MakerContract := encode.RandomBytes(36), encode.RandomBytes(50)
   545  	err = archie.SaveContractA(mktMatchID, MakerContract, MakerSwap, 0)
   546  	if err != nil {
   547  		t.Fatalf("SaveContractA error: %v", err)
   548  	}
   549  
   550  	TakerSwap, TakerContract := encode.RandomBytes(36), encode.RandomBytes(50)
   551  	err = archie.SaveContractB(mktMatchID, TakerContract, TakerSwap, 0)
   552  	if err != nil {
   553  		t.Fatalf("SaveContractB error: %v", err)
   554  	}
   555  
   556  	MakerRedeem, Secret := encode.RandomBytes(36), encode.RandomBytes(32)
   557  	err = archie.SaveRedeemA(mktMatchID, MakerRedeem, Secret, 0)
   558  	if err != nil {
   559  		t.Fatalf("SaveContractB error: %v", err)
   560  	}
   561  	// TakerRedeem not stored.
   562  
   563  	// Make another perfect 1 lot match on another market.
   564  	limitBuyStanding = newLimitOrderWithAssets(false, 4500000, 1, order.StandingTiF, 0, AssetBTC, AssetLTC)
   565  	limitSellImmediate = newLimitOrderWithAssets(true, 4490000, 1, order.ImmediateTiF, 10, AssetBTC, AssetLTC)
   566  
   567  	// Store it.
   568  	match = newMatch(limitBuyStanding, limitSellImmediate, limitSellImmediate.Quantity, epochID)
   569  	err = archie.InsertMatch(match)
   570  	if err != nil {
   571  		t.Fatalf("InsertMatch() failed: %v", err)
   572  	}
   573  
   574  	// Only active.
   575  	matchData, err := archie.MarketMatches(base, quote)
   576  	if err != nil {
   577  		t.Fatal(err)
   578  	}
   579  	if len(matchData) != 1 {
   580  		t.Errorf("Retrieved %d matches for market, expected 1.", len(matchData))
   581  	}
   582  	// Include inactive (true), and no limit (-1).
   583  	matchData = []*db.MatchDataWithCoins{}
   584  	N, err := archie.MarketMatchesStreaming(base, quote, true, -1, func(md *db.MatchDataWithCoins) error {
   585  		matchData = append(matchData, md)
   586  		return nil
   587  	})
   588  	if err != nil {
   589  		t.Fatal(err)
   590  	}
   591  	if N != len(matchData) {
   592  		t.Errorf("Retrieved %d matches for market, but method claimed %d.", len(matchData), N)
   593  	}
   594  	if len(matchData) != 2 {
   595  		t.Errorf("Retrieved %d matches for market, expected 2.", len(matchData))
   596  	}
   597  
   598  	// Find the match with the stored coins and verify them.
   599  	var found bool
   600  	for _, md := range matchData {
   601  		if md.ID == midWithCoins {
   602  			found = true
   603  			if !bytes.Equal(md.MakerSwapCoin, MakerSwap) {
   604  				t.Errorf("Wrong maker swap coin %x, wanted %x", md.MakerSwapCoin, MakerSwap)
   605  			}
   606  			if !bytes.Equal(md.TakerSwapCoin, TakerSwap) {
   607  				t.Errorf("Wrong taker swap coin %x, wanted %x", md.TakerSwapCoin, TakerSwap)
   608  			}
   609  			if !bytes.Equal(md.MakerRedeemCoin, MakerRedeem) {
   610  				t.Errorf("Wrong maker redeem coin %x, wanted %x", md.MakerRedeemCoin, MakerRedeem)
   611  			}
   612  			if len(md.TakerRedeemCoin) > 0 {
   613  				t.Errorf("got taker redeem coin %x, but expected none", md.TakerRedeemCoin)
   614  			}
   615  			break
   616  		}
   617  	}
   618  	if !found {
   619  		t.Errorf("failed to find match with the coins")
   620  	}
   621  
   622  	// Bad Market.
   623  	matchData, err = archie.MarketMatches(base, base)
   624  	noMktErr := new(db.ArchiveError)
   625  	if !errors.As(err, noMktErr) || noMktErr.Code != db.ErrUnsupportedMarket {
   626  		t.Fatalf("incorrect error for unsupported market: %v", err)
   627  	}
   628  }
   629  
   630  type matchPair struct {
   631  	match  *order.Match
   632  	status *db.MatchStatus
   633  }
   634  
   635  func generateMatch(t *testing.T, matchStatus order.MatchStatus, active bool, makerBuyer, takerSeller account.AccountID, epochIdx ...uint64) *matchPair {
   636  	t.Helper()
   637  	loBuy := newLimitOrder(false, 4500000, 1, order.StandingTiF, 0)
   638  	loBuy.P.AccountID = makerBuyer
   639  	loSell := newLimitOrder(true, 4490000, 1, order.ImmediateTiF, 10)
   640  	loSell.P.AccountID = takerSeller
   641  
   642  	epIdx := uint64(132412341)
   643  	if len(epochIdx) > 0 {
   644  		epIdx = epochIdx[0]
   645  	}
   646  	epochID := order.EpochID{epIdx, 1000}
   647  
   648  	err := archie.StoreOrder(loBuy, int64(epochID.Idx), int64(epochID.Dur), order.OrderStatusExecuted)
   649  	if err != nil {
   650  		t.Fatalf("failed to store order: %v", err)
   651  	}
   652  	err = archie.StoreOrder(loSell, int64(epochID.Idx), int64(epochID.Dur), order.OrderStatusExecuted)
   653  	if err != nil {
   654  		t.Fatalf("failed to store order: %v", err)
   655  	}
   656  
   657  	match := newMatch(loBuy, loSell, loSell.Quantity, epochID)
   658  	match.Status = matchStatus
   659  	err = archie.InsertMatch(match)
   660  	if err != nil {
   661  		t.Fatalf("InsertMatch() failed: %v", err)
   662  	}
   663  	matchID := match.ID()
   664  	mktMatchID := db.MarketMatchID{
   665  		MatchID: matchID,
   666  		Base:    loBuy.Base(),
   667  		Quote:   loBuy.Quote(),
   668  	}
   669  	// Just alternate the active state.
   670  	status := &db.MatchStatus{
   671  		Status: matchStatus,
   672  		Active: active,
   673  	}
   674  	if !active {
   675  		archie.SetMatchInactive(mktMatchID, false)
   676  	}
   677  	for iStatus := order.NewlyMatched; iStatus <= matchStatus; iStatus++ {
   678  		switch iStatus {
   679  		case order.MakerSwapCast:
   680  			status.MakerContract = encode.RandomBytes(50)
   681  			status.MakerSwap = encode.RandomBytes(36)
   682  			err := archie.SaveContractA(mktMatchID, status.MakerContract, status.MakerSwap, 0)
   683  			if err != nil {
   684  				t.Fatalf("SaveContractA error: %v", err)
   685  			}
   686  		case order.TakerSwapCast:
   687  			status.TakerContract = encode.RandomBytes(50)
   688  			status.TakerSwap = encode.RandomBytes(36)
   689  			err := archie.SaveContractB(mktMatchID, status.TakerContract, status.TakerSwap, 0)
   690  			if err != nil {
   691  				t.Fatalf("SaveContractB error: %v", err)
   692  			}
   693  		case order.MakerRedeemed:
   694  			status.MakerRedeem = encode.RandomBytes(36)
   695  			status.Secret = encode.RandomBytes(32)
   696  			err := archie.SaveRedeemA(mktMatchID, status.MakerRedeem, status.Secret, 0)
   697  			if err != nil {
   698  				t.Fatalf("SaveContractB error: %v", err)
   699  			}
   700  		case order.MatchComplete:
   701  			status.TakerRedeem = encode.RandomBytes(36)
   702  			err := archie.SaveRedeemB(mktMatchID, status.TakerRedeem, 0)
   703  			if err != nil {
   704  				t.Fatalf("SaveContractB error: %v", err)
   705  			}
   706  		}
   707  	}
   708  	return &matchPair{match: match, status: status}
   709  }
   710  
   711  func TestCompletedAndAtFaultMatchStats(t *testing.T) {
   712  	if err := cleanTables(archie.db); err != nil {
   713  		t.Fatalf("cleanTables: %v", err)
   714  	}
   715  
   716  	epIdx := uint64(132412341)
   717  	nextIdx := func() uint64 {
   718  		epIdx++
   719  		return epIdx
   720  	}
   721  
   722  	maker, taker := randomAccountID(), randomAccountID()
   723  	matches := []*matchPair{
   724  		generateMatch(t, order.TakerSwapCast, false, maker, taker, nextIdx()), // 0: failed, maker fault
   725  		generateMatch(t, order.MatchComplete, false, maker, taker, nextIdx()), // 1: success
   726  		generateMatch(t, order.MakerRedeemed, true, maker, taker, nextIdx()),  // 2: still active, but maker success
   727  		generateMatch(t, order.MakerRedeemed, false, maker, taker, nextIdx()), // 3: failed, maker success, taker fault
   728  		generateMatch(t, order.MakerRedeemed, false, maker, maker, nextIdx()), // 4: failed, maker fault (no same-user maker success until MatchComplete)
   729  		generateMatch(t, order.MakerSwapCast, false, maker, taker, nextIdx()), // 5: failed, taker fault
   730  		generateMatch(t, order.NewlyMatched, false, maker, taker, nextIdx()),  // 6: failed, maker fault
   731  	}
   732  
   733  	// Make a perfect 1 lot match in different market (BTC-LTC).
   734  	limitBuy := newLimitOrder(false, 4500000, 1, order.StandingTiF, 20)
   735  	limitBuy.BaseAsset, limitBuy.QuoteAsset = AssetBTC, AssetLTC
   736  	limitBuy.AccountID = maker
   737  	limitSell := newLimitOrder(true, 4490000, 1, order.ImmediateTiF, 30)
   738  	limitSell.BaseAsset, limitSell.QuoteAsset = AssetBTC, AssetLTC
   739  	taker2 := randomAccountID()
   740  	limitSell.AccountID = taker2
   741  	matchLTC := newMatch(limitBuy, limitSell, limitSell.Quantity, order.EpochID{nextIdx(), 1000})
   742  	matchLTC.Status = order.MatchComplete
   743  	err := archie.InsertMatch(matchLTC)
   744  	if err != nil {
   745  		t.Fatalf("InsertMatch() failed: %v", err)
   746  	}
   747  	archie.SetMatchInactive(db.MarketMatchID{
   748  		MatchID: matchLTC.ID(),
   749  		Base:    limitBuy.Base(),
   750  		Quote:   limitBuy.Quote(),
   751  	}, false)
   752  	// 7: success
   753  	matches = append(matches, &matchPair{
   754  		match: matchLTC,
   755  		status: &db.MatchStatus{
   756  			Active: false,
   757  			Status: matchLTC.Status,
   758  		},
   759  	})
   760  	// TODO: update with a forgiven one
   761  
   762  	epochTime := func(mp *matchPair) int64 {
   763  		return mp.match.Epoch.End().UnixMilli()
   764  	}
   765  
   766  	tests := []struct {
   767  		name         string
   768  		acctID       account.AccountID
   769  		wantOutcomes []*db.MatchOutcome
   770  		wantedErr    error
   771  	}{
   772  		{
   773  			"maker",
   774  			maker,
   775  			[]*db.MatchOutcome{ // descending by time (MatchID field TODO)
   776  				{
   777  					Status: matches[7].match.Status,
   778  					Fail:   false,
   779  					Time:   epochTime(matches[7]),
   780  				}, {
   781  					Status: matches[6].match.Status,
   782  					Fail:   true,
   783  					Time:   epochTime(matches[6]),
   784  				}, {
   785  					Status: matches[4].match.Status,
   786  					Fail:   true,
   787  					Time:   epochTime(matches[4]),
   788  				}, {
   789  					Status: matches[3].match.Status,
   790  					Fail:   false,
   791  					Time:   epochTime(matches[3]),
   792  				}, {
   793  					Status: matches[2].match.Status,
   794  					Fail:   false,
   795  					Time:   epochTime(matches[2]),
   796  				}, {
   797  					Status: matches[1].match.Status,
   798  					Fail:   false,
   799  					Time:   epochTime(matches[1]),
   800  				}, {
   801  					Status: matches[0].match.Status,
   802  					Fail:   true,
   803  					Time:   epochTime(matches[0]),
   804  				},
   805  			},
   806  			nil,
   807  		},
   808  		{
   809  			"taker",
   810  			taker,
   811  			[]*db.MatchOutcome{
   812  				{
   813  					Status: matches[5].match.Status,
   814  					Fail:   true,
   815  					Time:   epochTime(matches[5]),
   816  				}, {
   817  					Status: matches[3].match.Status,
   818  					Fail:   true,
   819  					Time:   epochTime(matches[3]),
   820  				}, {
   821  					Status: matches[1].match.Status,
   822  					Fail:   false,
   823  					Time:   epochTime(matches[1]),
   824  				},
   825  			},
   826  			nil,
   827  		},
   828  		{
   829  			"nope",
   830  			randomAccountID(),
   831  			nil,
   832  			nil,
   833  		},
   834  	}
   835  
   836  	for _, tt := range tests {
   837  		t.Run(tt.name, func(t *testing.T) {
   838  			outcomes, err := archie.CompletedAndAtFaultMatchStats(tt.acctID, 60)
   839  			if err != tt.wantedErr {
   840  				t.Fatal(err)
   841  			}
   842  			if len(outcomes) != len(tt.wantOutcomes) {
   843  				t.Errorf("Retrieved %d match outcomes for user %v, expected %d.", len(outcomes), tt.acctID, len(tt.wantOutcomes))
   844  			}
   845  			for i, mo := range tt.wantOutcomes {
   846  				if outcomes[i].Time != mo.Time || outcomes[i].Status != mo.Status || outcomes[i].Fail != mo.Fail {
   847  					t.Log(outcomes[i])
   848  					t.Log(mo)
   849  					t.Errorf("wrong %d", i)
   850  				}
   851  			}
   852  		})
   853  	}
   854  }
   855  
   856  func TestUserMatchFails(t *testing.T) {
   857  	if err := cleanTables(archie.db); err != nil {
   858  		t.Fatalf("cleanTables: %v", err)
   859  	}
   860  
   861  	epIdx := uint64(132412341)
   862  	nextIdx := func() uint64 {
   863  		epIdx++
   864  		return epIdx
   865  	}
   866  
   867  	user, otherUser := randomAccountID(), randomAccountID()
   868  	matches := []*matchPair{
   869  		generateMatch(t, order.TakerSwapCast, false, user, otherUser, nextIdx()), // 0: failed, user fault
   870  		generateMatch(t, order.MatchComplete, false, user, otherUser, nextIdx()), // 1: success
   871  		generateMatch(t, order.MakerRedeemed, true, user, otherUser, nextIdx()),  // 2: still active, but user success
   872  		generateMatch(t, order.MakerRedeemed, false, otherUser, user, nextIdx()), // 3: failed, user success, otherUser fault
   873  		generateMatch(t, order.MakerSwapCast, false, otherUser, user, nextIdx()), // 5: failed, user fault
   874  		generateMatch(t, order.NewlyMatched, false, otherUser, user, nextIdx()),  // 6: failed, otherUser fault
   875  	}
   876  	// Put one of them on another market
   877  	m4 := matches[4]
   878  	m4.match.Maker.Prefix().BaseAsset = AssetBTC
   879  	m4.match.Maker.Prefix().QuoteAsset = AssetLTC
   880  	m4.match.Taker.Prefix().BaseAsset = AssetBTC
   881  	m4.match.Taker.Prefix().QuoteAsset = AssetLTC
   882  	for _, m := range matches {
   883  		err := archie.InsertMatch(m.match)
   884  		if err != nil {
   885  			t.Fatalf("InsertMatch() failed: %v", err)
   886  		}
   887  	}
   888  	fails, err := archie.UserMatchFails(user, 100)
   889  	if err != nil {
   890  		t.Fatalf("UserMatchFails() failed: %v", err)
   891  	}
   892  check:
   893  	for _, i := range []int{0, 3, 4} {
   894  		matchID := matches[i].match.ID()
   895  		for _, fail := range fails {
   896  			if fail.ID == matchID {
   897  				continue check
   898  			}
   899  		}
   900  		t.Fatalf("expected to find fail for match at index %d, but did not", i)
   901  	}
   902  }
   903  
   904  func TestAllActiveUserMatches(t *testing.T) {
   905  	if err := cleanTables(archie.db); err != nil {
   906  		t.Fatalf("cleanTables: %v", err)
   907  	}
   908  
   909  	// Make a perfect 1 lot match.
   910  	limitBuyStanding := newLimitOrder(false, 4500000, 1, order.StandingTiF, 0)
   911  	limitSellImmediate := newLimitOrder(true, 4490000, 1, order.ImmediateTiF, 10)
   912  
   913  	// Make it complete and store it.
   914  	epochID := order.EpochID{132412341, 1000}
   915  	// maker buy (quote swap asset), taker sell (base swap asset)
   916  	match := newMatch(limitBuyStanding, limitSellImmediate, limitSellImmediate.Quantity, epochID)
   917  	match.Status = order.TakerSwapCast // failed here
   918  	err := archie.InsertMatch(match)   // active by default
   919  	if err != nil {
   920  		t.Fatalf("InsertMatch() failed: %v", err)
   921  	}
   922  	err = archie.SetMatchInactive(db.MatchID(match), false) // set inactive, not forgiven
   923  	if err != nil {
   924  		t.Fatalf("SetMatchInactive() failed: %v", err)
   925  	}
   926  
   927  	// Make a perfect 1 lot match, same parties.
   928  	limitBuyStanding2 := newLimitOrder(false, 4500000, 1, order.StandingTiF, 20)
   929  	limitBuyStanding2.AccountID = limitBuyStanding.AccountID
   930  	limitSellImmediate2 := newLimitOrder(true, 4490000, 1, order.ImmediateTiF, 30)
   931  	limitSellImmediate2.AccountID = limitSellImmediate.AccountID
   932  
   933  	// Store it.
   934  	epochID2 := order.EpochID{132412342, 1000}
   935  	// maker buy (quote swap asset), taker sell (base swap asset)
   936  	match2 := newMatch(limitBuyStanding2, limitSellImmediate2, limitSellImmediate2.Quantity, epochID2)
   937  	err = archie.InsertMatch(match2)
   938  	if err != nil {
   939  		t.Fatalf("InsertMatch() failed: %v", err)
   940  	}
   941  
   942  	// Make a perfect 1 lot BTC-LTC match.
   943  	limitBuyStanding3 := newLimitOrder(false, 4500000, 1, order.StandingTiF, 20)
   944  	limitBuyStanding3.BaseAsset = AssetBTC
   945  	limitBuyStanding3.QuoteAsset = AssetLTC
   946  	limitBuyStanding3.AccountID = limitBuyStanding.AccountID
   947  	limitSellImmediate3 := newLimitOrder(true, 4490000, 1, order.ImmediateTiF, 30)
   948  	limitSellImmediate3.BaseAsset = AssetBTC
   949  	limitSellImmediate3.QuoteAsset = AssetLTC
   950  	limitSellImmediate3.AccountID = limitSellImmediate.AccountID
   951  
   952  	// Store it.
   953  	epochID3 := order.EpochID{132412342, 1000}
   954  	match3 := newMatch(limitBuyStanding3, limitSellImmediate3, limitSellImmediate3.Quantity, epochID3)
   955  	err = archie.InsertMatch(match3)
   956  	if err != nil {
   957  		t.Fatalf("InsertMatch() failed: %v", err)
   958  	}
   959  
   960  	tests := []struct {
   961  		name        string
   962  		acctID      account.AccountID
   963  		numExpected int
   964  		wantMatch   []*order.Match
   965  		wantedErr   error
   966  	}{
   967  		{
   968  			"ok maker",
   969  			limitBuyStanding.User(),
   970  			2,
   971  			[]*order.Match{match2, match3},
   972  			nil,
   973  		},
   974  		{
   975  			"ok taker",
   976  			limitSellImmediate.User(),
   977  			2,
   978  			[]*order.Match{match2, match3},
   979  			nil,
   980  		},
   981  		{
   982  			"nope",
   983  			randomAccountID(),
   984  			0,
   985  			nil,
   986  			nil,
   987  		},
   988  	}
   989  
   990  	idInMatchSlice := func(mid order.MatchID, ms []*order.Match) int {
   991  		for i := range ms {
   992  			if ms[i].ID() == mid {
   993  				return i
   994  			}
   995  		}
   996  		return -1
   997  	}
   998  
   999  	for _, tt := range tests {
  1000  		t.Run(tt.name, func(t *testing.T) {
  1001  			userMatch, err := archie.AllActiveUserMatches(tt.acctID)
  1002  			if err != tt.wantedErr {
  1003  				t.Fatal(err)
  1004  			}
  1005  			if len(userMatch) != tt.numExpected {
  1006  				t.Errorf("Retrieved %d matches for user %v, expected %d.", len(userMatch), tt.acctID, tt.numExpected)
  1007  			}
  1008  			for _, match := range userMatch {
  1009  				loc := idInMatchSlice(match.ID, tt.wantMatch)
  1010  				if loc == -1 {
  1011  					t.Errorf("Unknown match ID retrieved: %v.", match.ID)
  1012  					continue
  1013  				}
  1014  				if tt.wantMatch[loc].FeeRateBase != match.BaseRate {
  1015  					t.Errorf("incorrect base fee rate. got %d, want %d",
  1016  						match.BaseRate, tt.wantMatch[loc].FeeRateBase)
  1017  				}
  1018  				if tt.wantMatch[loc].FeeRateQuote != match.QuoteRate {
  1019  					t.Errorf("incorrect quote fee rate. got %d, want %d",
  1020  						match.QuoteRate, tt.wantMatch[loc].FeeRateQuote)
  1021  				}
  1022  				if tt.wantMatch[loc].Epoch.End() != match.Epoch.End() {
  1023  					t.Errorf("incorrect match time. got %v, want %v",
  1024  						match.Epoch.End(), tt.wantMatch[loc].Epoch.End())
  1025  				}
  1026  				if tt.wantMatch[loc].Taker.Trade().Address != match.TakerAddr {
  1027  					t.Errorf("incorrect counterparty swap address. got %v, want %v",
  1028  						match.TakerAddr, tt.wantMatch[loc].Taker.Trade().Address)
  1029  				}
  1030  				if tt.wantMatch[loc].Maker.Address != match.MakerAddr {
  1031  					t.Errorf("incorrect counterparty swap address. got %v, want %v",
  1032  						match.MakerAddr, tt.wantMatch[loc].Maker.Address)
  1033  				}
  1034  			}
  1035  		})
  1036  	}
  1037  }
  1038  
  1039  func TestActiveSwaps(t *testing.T) {
  1040  	if err := cleanTables(archie.db); err != nil {
  1041  		t.Fatalf("cleanTables: %v", err)
  1042  	}
  1043  
  1044  	swapsDetails, err := archie.ActiveSwaps()
  1045  	if err != nil {
  1046  		t.Fatal(err)
  1047  	}
  1048  	if len(swapsDetails) > 0 {
  1049  		t.Fatalf("got details for %d swaps, expected 0", len(swapsDetails))
  1050  	}
  1051  
  1052  	user1 := randomAccountID()
  1053  	user2 := randomAccountID()
  1054  	match := generateMatch(t, order.MakerRedeemed, true, user1, user2)
  1055  
  1056  	swapsDetails, err = archie.ActiveSwaps()
  1057  	if err != nil {
  1058  		t.Fatal(err)
  1059  	}
  1060  	if len(swapsDetails) != 1 {
  1061  		t.Fatalf("got details for %d swaps, expected 1", len(swapsDetails))
  1062  	}
  1063  	swapDetails := swapsDetails[0]
  1064  
  1065  	taker, _, err := archie.Order(swapDetails.MatchData.Taker, swapDetails.Base, swapDetails.Quote)
  1066  	if err != nil {
  1067  		t.Fatalf("Failed to load taker order: %v", err)
  1068  	}
  1069  	if taker.ID() != swapDetails.MatchData.Taker {
  1070  		t.Fatalf("Failed to load order %v, computed ID %v instead", swapDetails.MatchData.Taker, taker.ID())
  1071  	}
  1072  	if match.match.Taker.ID() != swapDetails.MatchData.Taker {
  1073  		t.Fatalf("Failed to load order %v, computed ID %v instead", swapDetails.MatchData.Taker, taker.ID())
  1074  	}
  1075  
  1076  	maker, _, err := archie.Order(swapDetails.MatchData.Maker, swapDetails.Base, swapDetails.Quote)
  1077  	if err != nil {
  1078  		t.Fatalf("Failed to load maker order: %v", err)
  1079  	}
  1080  	if maker.ID() != swapDetails.MatchData.Maker {
  1081  		t.Fatalf("Failed to load order %v, computed ID %v instead", swapDetails.MatchData.Maker, maker.ID())
  1082  	}
  1083  	if match.match.Maker.ID() != swapDetails.MatchData.Maker {
  1084  		t.Fatalf("Failed to load order %v, computed ID %v instead", swapDetails.MatchData.Maker, maker.ID())
  1085  	}
  1086  
  1087  	if match.match.Rate != swapDetails.Rate {
  1088  		t.Fatalf("wrong rate loaded, got %d want %d", swapDetails.Rate, match.match.Rate)
  1089  	}
  1090  	if match.match.Quantity != swapDetails.Quantity {
  1091  		t.Fatalf("wrong quantity loaded, got %d want %d", swapDetails.Quantity, match.match.Quantity)
  1092  	}
  1093  	makerLO, ok := maker.(*order.LimitOrder)
  1094  	if !ok {
  1095  		t.Fatalf("Maker order was not a limit order: %T", maker)
  1096  	}
  1097  
  1098  	matchBack := &order.Match{
  1099  		Taker:        taker,
  1100  		Maker:        makerLO,
  1101  		Quantity:     swapDetails.Quantity,
  1102  		Rate:         swapDetails.Rate,
  1103  		FeeRateBase:  swapDetails.BaseRate,
  1104  		FeeRateQuote: swapDetails.QuoteRate,
  1105  		Epoch:        swapDetails.Epoch,
  1106  		Status:       swapDetails.Status,
  1107  		Sigs: order.Signatures{ // not really needed
  1108  			MakerMatch:  swapDetails.SwapData.SigMatchAckMaker,
  1109  			TakerMatch:  swapDetails.SwapData.SigMatchAckTaker,
  1110  			MakerAudit:  swapDetails.SwapData.ContractAAckSig,
  1111  			TakerAudit:  swapDetails.SwapData.ContractBAckSig,
  1112  			TakerRedeem: swapDetails.SwapData.RedeemAAckSig,
  1113  		},
  1114  	}
  1115  
  1116  	wantMid := match.match.ID()
  1117  	if wantMid != swapDetails.MatchData.ID {
  1118  		t.Fatalf("incorrect match ID %v, expected %v", swapDetails.MatchData.ID, wantMid)
  1119  	}
  1120  	// recompute the match ID from the loaded orders (their computed IDs), match rate, qty, etc.
  1121  	if wantMid != matchBack.ID() {
  1122  		t.Fatalf("Failed to reconstruct Match %v, computed ID %v instead", matchBack.ID(), wantMid)
  1123  	}
  1124  }
  1125  
  1126  func TestMatchStatuses(t *testing.T) {
  1127  	if err := cleanTables(archie.db); err != nil {
  1128  		t.Fatalf("cleanTables: %v", err)
  1129  	}
  1130  
  1131  	// Unknown market
  1132  	aid := randomAccountID()
  1133  	var mid order.MatchID
  1134  	copy(mid[:], encode.RandomBytes(32))
  1135  	_, err := archie.MatchStatuses(aid, 100, 101, []order.MatchID{mid})
  1136  	noMktErr := new(db.ArchiveError)
  1137  	if !errors.As(err, noMktErr) || noMktErr.Code != db.ErrUnsupportedMarket {
  1138  		t.Fatalf("incorrect error for unsupported market: %v", err)
  1139  	}
  1140  
  1141  	user1 := randomAccountID()
  1142  	user2 := randomAccountID()
  1143  
  1144  	matches := []*matchPair{
  1145  		generateMatch(t, order.NewlyMatched, true, user1, user2),                           // 0
  1146  		generateMatch(t, order.MakerSwapCast, false, user1, user2),                         // 1
  1147  		generateMatch(t, order.TakerSwapCast, true, user1, user2),                          // 2
  1148  		generateMatch(t, order.MakerRedeemed, true, user1, user2),                          // 3
  1149  		generateMatch(t, order.MatchComplete, false, user1, user2),                         // 4 -- inactive via SaveRedeemB
  1150  		generateMatch(t, order.MakerRedeemed, false, randomAccountID(), randomAccountID()), // 5
  1151  	}
  1152  
  1153  	idList := func(idxs ...int) []order.MatchID {
  1154  		ids := make([]order.MatchID, 0, len(idxs))
  1155  		for _, i := range idxs {
  1156  			ids = append(ids, matches[i].match.ID())
  1157  		}
  1158  		return ids
  1159  	}
  1160  
  1161  	tests := []struct {
  1162  		name string
  1163  		user account.AccountID
  1164  		req  []order.MatchID
  1165  		exp  []int // matches index
  1166  	}{
  1167  		// user 1: 1 hit
  1168  		{
  1169  			name: "find1",
  1170  			user: user1,
  1171  			req:  idList(0),
  1172  			exp:  []int{0},
  1173  		},
  1174  		// user 1: 1 hit + 1 miss.
  1175  		{
  1176  			name: "find1-miss1",
  1177  			user: user1,
  1178  			req:  idList(1, 5),
  1179  			exp:  []int{1},
  1180  		},
  1181  		// user 2 hit 4
  1182  		{
  1183  			name: "find4",
  1184  			user: user2,
  1185  			req:  idList(0, 1, 2, 3),
  1186  			exp:  []int{0, 1, 2, 3},
  1187  		},
  1188  	}
  1189  
  1190  	for _, tt := range tests {
  1191  		statuses, err := archie.MatchStatuses(tt.user, AssetDCR, AssetBTC, tt.req)
  1192  		if err != nil {
  1193  			t.Fatalf("%s: error getting order statuses: %v", tt.name, err)
  1194  		}
  1195  		if len(statuses) != len(tt.exp) {
  1196  			t.Fatalf("%s: wrongs number of statuses returned. expected %d, got %d", tt.name, len(tt.exp), len(statuses))
  1197  		}
  1198  	top:
  1199  		for _, expIdx := range tt.exp {
  1200  			matchPair := matches[expIdx]
  1201  			expStatus := matchPair.status
  1202  			matchID := matchPair.match.ID()
  1203  			// Find the status
  1204  			for _, status := range statuses {
  1205  				if status.ID != matchID {
  1206  					continue
  1207  				}
  1208  				if status.Status != expStatus.Status {
  1209  					t.Fatalf("%s: expIdx = %d, wrong status. expected %s, got %s", tt.name, expIdx, expStatus.Status, status.Status)
  1210  				}
  1211  				if !bytes.Equal(status.MakerContract, expStatus.MakerContract) {
  1212  					t.Fatalf("%s: wrong MakerContract. expected %x, got %x", tt.name, expStatus.MakerContract, status.MakerContract)
  1213  				}
  1214  				if !bytes.Equal(status.TakerContract, expStatus.TakerContract) {
  1215  					t.Fatalf("%s: wrong TakerContract. expected %x, got %x", tt.name, expStatus.TakerContract, status.TakerContract)
  1216  				}
  1217  				if !bytes.Equal(status.MakerSwap, expStatus.MakerSwap) {
  1218  					t.Fatalf("%s: wrong MakerSwap. expected %x, got %x", tt.name, expStatus.MakerSwap, status.MakerSwap)
  1219  				}
  1220  				if !bytes.Equal(status.TakerSwap, expStatus.TakerSwap) {
  1221  					t.Fatalf("%s: wrong TakerSwap. expected %x, got %x", tt.name, expStatus.TakerSwap, status.TakerSwap)
  1222  				}
  1223  				if !bytes.Equal(status.MakerRedeem, expStatus.MakerRedeem) {
  1224  					t.Fatalf("%s: wrong MakerRedeem. expected %x, got %x", tt.name, expStatus.MakerRedeem, status.MakerRedeem)
  1225  				}
  1226  				if !bytes.Equal(status.TakerRedeem, expStatus.TakerRedeem) {
  1227  					t.Fatalf("%s: wrong TakerRedeem. expected %x, got %x", tt.name, expStatus.TakerRedeem, status.TakerRedeem)
  1228  				}
  1229  				if !bytes.Equal(status.Secret, expStatus.Secret) {
  1230  					t.Fatalf("%s: wrong Secret. expected %x, got %x", tt.name, expStatus.Secret, status.Secret)
  1231  				}
  1232  				if status.Active != expStatus.Active {
  1233  					t.Fatalf("%s: wrong Active. expected %t, got %t", tt.name, expStatus.Active, status.Active)
  1234  				}
  1235  				continue top
  1236  			}
  1237  			t.Fatalf("%s: expected match at index %d not found in results", tt.name, expIdx)
  1238  		}
  1239  	}
  1240  
  1241  }
  1242  
  1243  func TestEpochReport(t *testing.T) {
  1244  	if err := cleanTables(archie.db); err != nil {
  1245  		t.Fatalf("cleanTables: %v", err)
  1246  	}
  1247  
  1248  	lastRate, err := archie.LastEpochRate(42, 0)
  1249  	if err != nil {
  1250  		t.Fatalf("error getting last epoch rate from empty table (should be err = nil, rate = 0): %v", err)
  1251  	}
  1252  	if lastRate != 0 {
  1253  		t.Fatalf("wrong initial last rate. expected 0, got %d", lastRate)
  1254  	}
  1255  
  1256  	var epochIdx, epochDur int64 = 13245678, 6000
  1257  	err = archie.InsertEpoch(&db.EpochResults{
  1258  		MktBase:     42,
  1259  		MktQuote:    0,
  1260  		Idx:         epochIdx,
  1261  		Dur:         epochDur,
  1262  		MatchVolume: 1,
  1263  		HighRate:    2,
  1264  		LowRate:     3,
  1265  		StartRate:   4,
  1266  		EndRate:     5,
  1267  		QuoteVolume: 6,
  1268  	})
  1269  
  1270  	if err != nil {
  1271  		t.Fatalf("error inserting first epoch: %v", err)
  1272  	}
  1273  
  1274  	lastRate, err = archie.LastEpochRate(42, 0)
  1275  	if err != nil {
  1276  		t.Fatalf("error getting last epoch rate from after first epoch: %v", err)
  1277  	}
  1278  	if lastRate != 5 {
  1279  		t.Fatalf("wrong first epoch last rate. expected 5, got %d", lastRate)
  1280  	}
  1281  
  1282  	// Trying for the same epoch should violate a primary key constraint.
  1283  	err = archie.InsertEpoch(&db.EpochResults{
  1284  		MktBase:  42,
  1285  		MktQuote: 0,
  1286  		Idx:      epochIdx,
  1287  		Dur:      epochDur,
  1288  	})
  1289  	if err == nil {
  1290  		t.Fatalf("no error for duplicate epoch")
  1291  	}
  1292  
  1293  	err = archie.InsertEpoch(&db.EpochResults{
  1294  		MktBase:     42,
  1295  		MktQuote:    0,
  1296  		Idx:         epochIdx + 1,
  1297  		Dur:         epochDur,
  1298  		MatchVolume: 11,
  1299  		HighRate:    12,
  1300  		LowRate:     13,
  1301  		StartRate:   14,
  1302  		EndRate:     15,
  1303  		QuoteVolume: 16,
  1304  	})
  1305  	if err != nil {
  1306  		t.Fatalf("error inserting second epoch: %v", err)
  1307  	}
  1308  
  1309  	lastRate, err = archie.LastEpochRate(42, 0)
  1310  	if err != nil {
  1311  		t.Fatalf("error getting last epoch rate from after second-to-last epoch: %v", err)
  1312  	}
  1313  	if lastRate != 15 {
  1314  		t.Fatalf("wrong second-to-last epoch last rate. expected 15, got %d", lastRate)
  1315  	}
  1316  
  1317  	archie.InsertEpoch(&db.EpochResults{
  1318  		MktBase:     42,
  1319  		MktQuote:    0,
  1320  		Idx:         epochIdx + 2,
  1321  		Dur:         epochDur,
  1322  		MatchVolume: 100,
  1323  		HighRate:    100,
  1324  		LowRate:     100,
  1325  		StartRate:   100,
  1326  		EndRate:     100,
  1327  		QuoteVolume: 100,
  1328  	})
  1329  
  1330  	epochCache := candles.NewCache(3, uint64(epochDur))
  1331  	dayCache := candles.NewCache(2, uint64(time.Hour*24/time.Millisecond))
  1332  
  1333  	err = archie.LoadEpochStats(42, 0, []*candles.Cache{epochCache, dayCache})
  1334  	if err != nil {
  1335  		t.Fatalf("error loading epoch stats: %v", err)
  1336  	}
  1337  
  1338  	epochCandles := epochCache.WireCandles(3).Candles()
  1339  	if len(epochCandles) != 3 {
  1340  		t.Fatalf("epoch cache has wrong number of entries. expected 3, got %d", len(epochCandles))
  1341  	}
  1342  	lastCandle := epochCandles[len(epochCandles)-1]
  1343  	if lastCandle.MatchVolume != 100 {
  1344  		t.Fatalf("wrong last epoch candle match volume. expected 100, got %d", lastCandle.MatchVolume)
  1345  	}
  1346  
  1347  	dayCandles := dayCache.WireCandles(2).Candles()
  1348  	if len(dayCandles) != 1 {
  1349  		t.Fatalf("day cache has wrong number of entries. expected 1, got %d", len(dayCandles))
  1350  	}
  1351  	lastCandle = dayCandles[len(dayCandles)-1]
  1352  	if lastCandle.MatchVolume != 112 { // 1 + 11
  1353  		t.Fatalf("wrong last day candle MatchVolume. expected 112, got %d", lastCandle.MatchVolume)
  1354  	}
  1355  	if lastCandle.QuoteVolume != 122 { // 6 + 16
  1356  		t.Fatalf("wrong last day candle QuoteVolume. expected 122, got %d", lastCandle.MatchVolume)
  1357  	}
  1358  	if lastCandle.HighRate != 100 {
  1359  		t.Fatalf("wrong last day candle HighRate. expected 100, got %d", lastCandle.HighRate)
  1360  	}
  1361  	if lastCandle.LowRate != 3 {
  1362  		t.Fatalf("wrong last day candle LowRate. expected 3, got %d", lastCandle.LowRate)
  1363  	}
  1364  	if lastCandle.StartRate != 4 {
  1365  		t.Fatalf("wrong last day candle StartRate. expected 4, got %d", lastCandle.StartRate)
  1366  	}
  1367  	if lastCandle.EndRate != 100 {
  1368  		t.Fatalf("wrong last day candle EndRate. expected 100, got %d", lastCandle.EndRate)
  1369  	}
  1370  
  1371  }