decred.org/dcrdex@v1.0.5/server/db/driver/pg/matches.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 pg
     5  
     6  import (
     7  	"context"
     8  	"database/sql"
     9  	"errors"
    10  	"fmt"
    11  	"math"
    12  	"sort"
    13  
    14  	"decred.org/dcrdex/dex/order"
    15  	"decred.org/dcrdex/server/account"
    16  	"decred.org/dcrdex/server/db"
    17  	"decred.org/dcrdex/server/db/driver/pg/internal"
    18  	"github.com/lib/pq"
    19  )
    20  
    21  func (a *Archiver) matchTableName(match *order.Match) (string, error) {
    22  	marketSchema, err := a.marketSchema(match.Maker.Base(), match.Maker.Quote())
    23  	if err != nil {
    24  		return "", err
    25  	}
    26  	return fullMatchesTableName(a.dbName, marketSchema), nil
    27  }
    28  
    29  // ForgiveMatchFail marks the specified match as forgiven. Since this is an
    30  // administrative function, the burden is on the operator to ensure the match
    31  // can actually be forgiven (inactive, not already forgiven, and not in
    32  // MatchComplete status).
    33  func (a *Archiver) ForgiveMatchFail(mid order.MatchID) (bool, error) {
    34  	for schema := range a.markets {
    35  		stmt := fmt.Sprintf(internal.ForgiveMatchFail, fullMatchesTableName(a.dbName, schema))
    36  		N, err := sqlExec(a.db, stmt, mid)
    37  		if err != nil { // not just no rows updated
    38  			return false, err
    39  		}
    40  		if N == 1 {
    41  			return true, nil
    42  		} // N > 1 cannot happen since matchid is the primary key
    43  		// N==0 could also mean it was not eligible to forgive, but just keep going
    44  	}
    45  	return false, nil
    46  }
    47  
    48  // ActiveSwaps loads the full details for all active swaps across all markets.
    49  func (a *Archiver) ActiveSwaps() ([]*db.SwapDataFull, error) {
    50  	var sd []*db.SwapDataFull
    51  
    52  	for schema, mkt := range a.markets {
    53  		matchesTableName := fullMatchesTableName(a.dbName, schema)
    54  		ctx, cancel := context.WithTimeout(a.ctx, a.queryTimeout)
    55  		matches, swapData, err := activeSwaps(ctx, a.db, matchesTableName)
    56  		cancel()
    57  		if err != nil {
    58  			return nil, err
    59  		}
    60  
    61  		for i := range matches {
    62  			sd = append(sd, &db.SwapDataFull{
    63  				Base:      mkt.Base,
    64  				Quote:     mkt.Quote,
    65  				MatchData: matches[i],
    66  				SwapData:  swapData[i],
    67  			})
    68  		}
    69  	}
    70  
    71  	return sd, nil
    72  }
    73  
    74  func activeSwaps(ctx context.Context, dbe *sql.DB, tableName string) (matches []*db.MatchData, swapData []*db.SwapData, err error) {
    75  	stmt := fmt.Sprintf(internal.RetrieveActiveMarketMatchesExtended, tableName)
    76  	rows, err := dbe.QueryContext(ctx, stmt)
    77  	if err != nil {
    78  		return
    79  	}
    80  	defer rows.Close()
    81  
    82  	for rows.Next() {
    83  		var m db.MatchData
    84  		var sd db.SwapData
    85  
    86  		var status uint8
    87  		var baseRate, quoteRate sql.NullInt64
    88  		var takerSell sql.NullBool
    89  		var takerAddr, makerAddr sql.NullString
    90  		var contractATime, contractBTime, redeemATime, redeemBTime sql.NullInt64
    91  
    92  		err = rows.Scan(&m.ID, &takerSell,
    93  			&m.Taker, &m.TakerAcct, &takerAddr,
    94  			&m.Maker, &m.MakerAcct, &makerAddr,
    95  			&m.Epoch.Idx, &m.Epoch.Dur, &m.Quantity, &m.Rate,
    96  			&baseRate, &quoteRate, &status,
    97  			&sd.SigMatchAckMaker, &sd.SigMatchAckTaker,
    98  			&sd.ContractACoinID, &sd.ContractA, &contractATime,
    99  			&sd.ContractAAckSig,
   100  			&sd.ContractBCoinID, &sd.ContractB, &contractBTime,
   101  			&sd.ContractBAckSig,
   102  			&sd.RedeemACoinID, &sd.RedeemASecret, &redeemATime,
   103  			&sd.RedeemAAckSig,
   104  			&sd.RedeemBCoinID, &redeemBTime)
   105  		if err != nil {
   106  			return nil, nil, err
   107  		}
   108  
   109  		// All are active.
   110  		m.Active = true
   111  
   112  		m.Status = order.MatchStatus(status)
   113  		m.TakerSell = takerSell.Bool
   114  		m.TakerAddr = takerAddr.String
   115  		m.MakerAddr = makerAddr.String
   116  		m.BaseRate = uint64(baseRate.Int64)
   117  		m.QuoteRate = uint64(quoteRate.Int64)
   118  
   119  		sd.ContractATime = contractATime.Int64
   120  		sd.ContractBTime = contractBTime.Int64
   121  		sd.RedeemATime = redeemATime.Int64
   122  		sd.RedeemBTime = redeemBTime.Int64
   123  
   124  		matches = append(matches, &m)
   125  		swapData = append(swapData, &sd)
   126  	}
   127  
   128  	if err = rows.Err(); err != nil {
   129  		return nil, nil, err
   130  	}
   131  
   132  	return
   133  }
   134  
   135  // CompletedAndAtFaultMatchStats retrieves the outcomes of matches that were (1)
   136  // successfully completed by the specified user, or (2) failed with the user
   137  // being the at-fault party. Note that the MakerRedeemed match status may be
   138  // either a success or failure depending on if the user was the maker or taker
   139  // in the swap, respectively, and the MatchOutcome.Fail flag disambiguates this.
   140  func (a *Archiver) CompletedAndAtFaultMatchStats(aid account.AccountID, lastN int) ([]*db.MatchOutcome, error) {
   141  	var outcomes []*db.MatchOutcome
   142  
   143  	for schema, mkt := range a.markets {
   144  		matchesTableName := fullMatchesTableName(a.dbName, schema)
   145  		ctx, cancel := context.WithTimeout(a.ctx, a.queryTimeout)
   146  		matchOutcomes, err := completedAndAtFaultMatches(ctx, a.db, matchesTableName, aid, lastN, mkt.Base, mkt.Quote)
   147  		cancel()
   148  		if err != nil {
   149  			return nil, err
   150  		}
   151  
   152  		outcomes = append(outcomes, matchOutcomes...)
   153  	}
   154  
   155  	sort.Slice(outcomes, func(i, j int) bool {
   156  		return outcomes[j].Time < outcomes[i].Time // descending
   157  	})
   158  	if len(outcomes) > lastN {
   159  		outcomes = outcomes[:lastN]
   160  	}
   161  	return outcomes, nil
   162  }
   163  
   164  // UserMatchFails retrieves up to the last n most recent failed and unforgiven
   165  // match outcomes for the user.
   166  func (a *Archiver) UserMatchFails(aid account.AccountID, lastN int) ([]*db.MatchFail, error) {
   167  	var fails []*db.MatchFail
   168  
   169  	for schema := range a.markets {
   170  		matchesTableName := fullMatchesTableName(a.dbName, schema)
   171  		ctx, cancel := context.WithTimeout(a.ctx, a.queryTimeout)
   172  		marketFails, err := atFaultMatches(ctx, a.db, matchesTableName, aid, lastN)
   173  		cancel()
   174  		if err != nil {
   175  			return nil, err
   176  		}
   177  
   178  		fails = append(fails, marketFails...)
   179  	}
   180  
   181  	if len(fails) > lastN {
   182  		fails = fails[:lastN]
   183  	}
   184  	return fails, nil
   185  }
   186  
   187  func completedAndAtFaultMatches(ctx context.Context, dbe *sql.DB, tableName string,
   188  	aid account.AccountID, lastN int, base, quote uint32) (outcomes []*db.MatchOutcome, err error) {
   189  	stmt := fmt.Sprintf(internal.CompletedOrAtFaultMatchesLastN, tableName)
   190  	rows, err := dbe.QueryContext(ctx, stmt, aid, lastN)
   191  	if err != nil {
   192  		return
   193  	}
   194  	defer rows.Close()
   195  
   196  	for rows.Next() {
   197  		var status uint8
   198  		var success bool
   199  		var refTime sql.NullInt64
   200  		var mid order.MatchID
   201  		var value uint64
   202  		err = rows.Scan(&mid, &status, &value, &success, &refTime)
   203  		if err != nil {
   204  			return
   205  		}
   206  
   207  		if !refTime.Valid {
   208  			continue // should not happen as all matches will have an epoch time, but don't error
   209  		}
   210  
   211  		// A little seat belt in case the query returns inconsistent results
   212  		// where success and status don't jive.
   213  		switch order.MatchStatus(status) {
   214  		case order.NewlyMatched, order.MakerSwapCast, order.TakerSwapCast:
   215  			if success {
   216  				log.Errorf("successfully completed match in status %v returned from DB", status)
   217  				continue
   218  			}
   219  		// MakerRedeemed can be either depending on user role (maker/taker).
   220  		case order.MatchComplete:
   221  			if !success {
   222  				log.Errorf("failed match in status %v returned from DB", status)
   223  				continue
   224  			}
   225  		}
   226  
   227  		outcomes = append(outcomes, &db.MatchOutcome{
   228  			Status: order.MatchStatus(status),
   229  			ID:     mid,
   230  			Fail:   !success,
   231  			Time:   refTime.Int64,
   232  			Value:  value,
   233  			Base:   base,
   234  			Quote:  quote,
   235  		})
   236  	}
   237  
   238  	if err = rows.Err(); err != nil {
   239  		return nil, err
   240  	}
   241  
   242  	return
   243  }
   244  
   245  func atFaultMatches(ctx context.Context, dbe *sql.DB, tableName string, aid account.AccountID, lastN int) (fails []*db.MatchFail, err error) {
   246  	stmt := fmt.Sprintf(internal.UserMatchFails, tableName)
   247  	rows, err := dbe.QueryContext(ctx, stmt, aid, lastN)
   248  	if err != nil {
   249  		return
   250  	}
   251  	defer rows.Close()
   252  
   253  	for rows.Next() {
   254  		var status uint8
   255  		var mid order.MatchID
   256  		err = rows.Scan(&mid, &status)
   257  		if err != nil {
   258  			return
   259  		}
   260  
   261  		fails = append(fails, &db.MatchFail{
   262  			Status: order.MatchStatus(status),
   263  			ID:     mid,
   264  		})
   265  	}
   266  
   267  	if err = rows.Err(); err != nil {
   268  		return nil, err
   269  	}
   270  
   271  	return
   272  }
   273  
   274  // UserMatches retrieves all matches involving a user on the given market.
   275  // TODO: consider a time limited version of this to retrieve recent matches.
   276  func (a *Archiver) UserMatches(aid account.AccountID, base, quote uint32) ([]*db.MatchData, error) {
   277  	marketSchema, err := a.marketSchema(base, quote)
   278  	if err != nil {
   279  		return nil, err
   280  	}
   281  
   282  	matchesTableName := fullMatchesTableName(a.dbName, marketSchema)
   283  
   284  	ctx, cancel := context.WithTimeout(a.ctx, a.queryTimeout)
   285  	defer cancel()
   286  
   287  	return userMatches(ctx, a.db, matchesTableName, aid, true)
   288  }
   289  
   290  func userMatches(ctx context.Context, dbe *sql.DB, tableName string, aid account.AccountID, includeInactive bool) ([]*db.MatchData, error) {
   291  	query := internal.RetrieveActiveUserMatches
   292  	if includeInactive {
   293  		query = internal.RetrieveUserMatches
   294  	}
   295  	stmt := fmt.Sprintf(query, tableName)
   296  	rows, err := dbe.QueryContext(ctx, stmt, aid)
   297  	if err != nil {
   298  		return nil, err
   299  	}
   300  	return rowsToMatchData(rows, includeInactive)
   301  }
   302  
   303  func rowsToMatchData(rows *sql.Rows, includeInactive bool) ([]*db.MatchData, error) {
   304  	defer rows.Close()
   305  
   306  	var (
   307  		ms  []*db.MatchData
   308  		err error
   309  	)
   310  	for rows.Next() {
   311  		var m db.MatchData
   312  		var status uint8
   313  		var baseRate, quoteRate sql.NullInt64
   314  		var takerSell sql.NullBool
   315  		var takerAddr, makerAddr sql.NullString
   316  		if includeInactive {
   317  			// "active" column SELECTed.
   318  			err = rows.Scan(&m.ID, &m.Active, &takerSell,
   319  				&m.Taker, &m.TakerAcct, &takerAddr,
   320  				&m.Maker, &m.MakerAcct, &makerAddr,
   321  				&m.Epoch.Idx, &m.Epoch.Dur, &m.Quantity, &m.Rate,
   322  				&baseRate, &quoteRate, &status)
   323  			if err != nil {
   324  				return nil, err
   325  			}
   326  		} else {
   327  			// "active" column not SELECTed.
   328  			err = rows.Scan(&m.ID, &takerSell,
   329  				&m.Taker, &m.TakerAcct, &takerAddr,
   330  				&m.Maker, &m.MakerAcct, &makerAddr,
   331  				&m.Epoch.Idx, &m.Epoch.Dur, &m.Quantity, &m.Rate,
   332  				&baseRate, &quoteRate, &status)
   333  			if err != nil {
   334  				return nil, err
   335  			}
   336  			// All are active.
   337  			m.Active = true
   338  		}
   339  		m.Status = order.MatchStatus(status)
   340  		m.TakerSell = takerSell.Bool
   341  		m.TakerAddr = takerAddr.String
   342  		m.MakerAddr = makerAddr.String
   343  		m.BaseRate = uint64(baseRate.Int64)
   344  		m.QuoteRate = uint64(quoteRate.Int64)
   345  
   346  		ms = append(ms, &m)
   347  	}
   348  
   349  	if err = rows.Err(); err != nil {
   350  		return nil, err
   351  	}
   352  
   353  	return ms, nil
   354  }
   355  
   356  func (a *Archiver) marketMatches(base, quote uint32, includeInactive bool, N int64, f func(*db.MatchDataWithCoins) error) (int, error) {
   357  	marketSchema, err := a.marketSchema(base, quote)
   358  	if err != nil {
   359  		return 0, err
   360  	}
   361  
   362  	matchesTableName := fullMatchesTableName(a.dbName, marketSchema)
   363  
   364  	ctx, cancel := context.WithTimeout(a.ctx, a.queryTimeout)
   365  	defer cancel()
   366  
   367  	var rows *sql.Rows
   368  	if includeInactive {
   369  		stmt := fmt.Sprintf(internal.RetrieveMarketMatches, matchesTableName)
   370  		if N <= 0 {
   371  			N = math.MaxInt64
   372  		}
   373  		rows, err = a.db.QueryContext(ctx, stmt, N)
   374  	} else {
   375  		stmt := fmt.Sprintf(internal.RetrieveActiveMarketMatches, matchesTableName)
   376  		rows, err = a.db.QueryContext(ctx, stmt) // no N
   377  	}
   378  	if err != nil {
   379  		return 0, err
   380  	}
   381  
   382  	return rowsToMatchDataWithCoinsStreaming(rows, includeInactive, f)
   383  }
   384  
   385  // MarketMatches retrieves all active matches for a market.
   386  func (a *Archiver) MarketMatches(base, quote uint32) ([]*db.MatchDataWithCoins, error) {
   387  	var ms []*db.MatchDataWithCoins
   388  	f := func(m *db.MatchDataWithCoins) error {
   389  		ms = append(ms, m)
   390  		return nil
   391  	}
   392  	_, err := a.marketMatches(base, quote, false, -1, f) // N ignored with only active
   393  	if err != nil {
   394  		return nil, err
   395  	}
   396  	return ms, nil
   397  }
   398  
   399  // MarketMatchesStreaming streams all active matches for a market into the
   400  // provided function. If includeInactive, all matches are streamed. A limit may
   401  // be specified, where <=0 means unlimited.
   402  func (a *Archiver) MarketMatchesStreaming(base, quote uint32, includeInactive bool, N int64, f func(*db.MatchDataWithCoins) error) (int, error) {
   403  	return a.marketMatches(base, quote, includeInactive, N, f)
   404  }
   405  
   406  func rowsToMatchDataWithCoinsStreaming(rows *sql.Rows, includeInactive bool, f func(*db.MatchDataWithCoins) error) (int, error) {
   407  	defer rows.Close()
   408  
   409  	var N int
   410  	for rows.Next() {
   411  		var m db.MatchDataWithCoins
   412  		var status uint8
   413  		var baseRate, quoteRate sql.NullInt64
   414  		var takerSell sql.NullBool
   415  		var takerAddr, makerAddr sql.NullString
   416  		if includeInactive {
   417  			// "active" column SELECTed.
   418  			err := rows.Scan(&m.ID, &m.Active, &takerSell,
   419  				&m.Taker, &m.TakerAcct, &takerAddr,
   420  				&m.Maker, &m.MakerAcct, &makerAddr,
   421  				&m.Epoch.Idx, &m.Epoch.Dur, &m.Quantity, &m.Rate,
   422  				&baseRate, &quoteRate, &status,
   423  				&m.MakerSwapCoin, &m.TakerSwapCoin, &m.MakerRedeemCoin, &m.TakerRedeemCoin)
   424  			if err != nil {
   425  				return N, err
   426  			}
   427  		} else {
   428  			// "active" column not SELECTed.
   429  			err := rows.Scan(&m.ID, &takerSell,
   430  				&m.Taker, &m.TakerAcct, &takerAddr,
   431  				&m.Maker, &m.MakerAcct, &makerAddr,
   432  				&m.Epoch.Idx, &m.Epoch.Dur, &m.Quantity, &m.Rate,
   433  				&baseRate, &quoteRate, &status,
   434  				&m.MakerSwapCoin, &m.TakerSwapCoin, &m.MakerRedeemCoin, &m.TakerRedeemCoin)
   435  			if err != nil {
   436  				return N, err
   437  			}
   438  			// All are active.
   439  			m.Active = true
   440  		}
   441  		m.Status = order.MatchStatus(status)
   442  		m.TakerSell = takerSell.Bool
   443  		m.TakerAddr = takerAddr.String
   444  		m.MakerAddr = makerAddr.String
   445  		m.BaseRate = uint64(baseRate.Int64)
   446  		m.QuoteRate = uint64(quoteRate.Int64)
   447  
   448  		if err := f(&m); err != nil {
   449  			return N, err
   450  		}
   451  		N++
   452  	}
   453  
   454  	return N, rows.Err()
   455  }
   456  
   457  // AllActiveUserMatches retrieves a MatchData slice for active matches in all
   458  // markets involving the given user. Swaps that have successfully completed or
   459  // failed are not included.
   460  func (a *Archiver) AllActiveUserMatches(aid account.AccountID) ([]*db.MatchData, error) {
   461  	ctx, cancel := context.WithTimeout(a.ctx, a.queryTimeout)
   462  	defer cancel()
   463  
   464  	var matches []*db.MatchData
   465  	for schema := range a.markets {
   466  		matchesTableName := fullMatchesTableName(a.dbName, schema)
   467  		mdM, err := userMatches(ctx, a.db, matchesTableName, aid, false)
   468  		if err != nil {
   469  			return nil, err
   470  		}
   471  
   472  		matches = append(matches, mdM...)
   473  	}
   474  
   475  	return matches, nil
   476  }
   477  
   478  // MatchStatuses retrieves a *db.MatchStatus for every match in matchIDs for
   479  // which there is data, and for which the user is at least one of the parties.
   480  // It is not an error if a match ID in matchIDs does not match, i.e. the
   481  // returned slice need not be the same length as matchIDs.
   482  func (a *Archiver) MatchStatuses(aid account.AccountID, base, quote uint32, matchIDs []order.MatchID) ([]*db.MatchStatus, error) {
   483  	ctx, cancel := context.WithTimeout(a.ctx, a.queryTimeout)
   484  	defer cancel()
   485  
   486  	marketSchema, err := a.marketSchema(base, quote)
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  
   491  	matchesTableName := fullMatchesTableName(a.dbName, marketSchema)
   492  	return matchStatusesByID(ctx, a.db, aid, matchesTableName, matchIDs)
   493  
   494  }
   495  
   496  func upsertMatch(dbe sqlExecutor, tableName string, match *order.Match) (int64, error) {
   497  	var takerAddr string
   498  	tt := match.Taker.Trade()
   499  	if tt != nil {
   500  		takerAddr = tt.SwapAddress()
   501  	}
   502  
   503  	// Cancel orders do not store taker or maker addresses, and are stored with
   504  	// complete status with no active swap negotiation.
   505  	if takerAddr == "" {
   506  		stmt := fmt.Sprintf(internal.UpsertCancelMatch, tableName)
   507  		return sqlExec(dbe, stmt, match.ID(),
   508  			match.Taker.ID(), match.Taker.User(), // taker address remains unset/default
   509  			match.Maker.ID(), match.Maker.User(), // as does maker's since it is not used
   510  			match.Epoch.Idx, match.Epoch.Dur,
   511  			int64(match.Quantity), int64(match.Rate), // quantity and rate may be useful for cancel statistics however
   512  			int8(order.MatchComplete)) // status is complete
   513  	}
   514  
   515  	stmt := fmt.Sprintf(internal.UpsertMatch, tableName)
   516  	return sqlExec(dbe, stmt, match.ID(), tt.Sell,
   517  		match.Taker.ID(), match.Taker.User(), takerAddr,
   518  		match.Maker.ID(), match.Maker.User(), match.Maker.Trade().SwapAddress(),
   519  		match.Epoch.Idx, match.Epoch.Dur,
   520  		int64(match.Quantity), int64(match.Rate),
   521  		match.FeeRateBase, match.FeeRateQuote, int8(match.Status))
   522  }
   523  
   524  // InsertMatch updates an existing match.
   525  func (a *Archiver) InsertMatch(match *order.Match) error {
   526  	matchesTableName, err := a.matchTableName(match)
   527  	if err != nil {
   528  		return err
   529  	}
   530  	N, err := upsertMatch(a.db, matchesTableName, match)
   531  	if err != nil {
   532  		a.fatalBackendErr(err)
   533  		return err
   534  	}
   535  	if N != 1 {
   536  		return fmt.Errorf("upsertMatch: updated %d rows, expected 1", N)
   537  	}
   538  	return nil
   539  }
   540  
   541  // MatchByID retrieves the match for the given MatchID.
   542  func (a *Archiver) MatchByID(mid order.MatchID, base, quote uint32) (*db.MatchData, error) {
   543  	marketSchema, err := a.marketSchema(base, quote)
   544  	if err != nil {
   545  		return nil, err
   546  	}
   547  
   548  	matchesTableName := fullMatchesTableName(a.dbName, marketSchema)
   549  	matchData, err := matchByID(a.db, matchesTableName, mid)
   550  	if errors.Is(err, sql.ErrNoRows) {
   551  		err = db.ArchiveError{Code: db.ErrUnknownMatch}
   552  	}
   553  	return matchData, err
   554  }
   555  
   556  func matchByID(dbe *sql.DB, tableName string, mid order.MatchID) (*db.MatchData, error) {
   557  	var m db.MatchData
   558  	var status uint8
   559  	var baseRate, quoteRate sql.NullInt64
   560  	var takerAddr, makerAddr sql.NullString
   561  	var takerSell sql.NullBool
   562  	stmt := fmt.Sprintf(internal.RetrieveMatchByID, tableName)
   563  	err := dbe.QueryRow(stmt, mid).
   564  		Scan(&m.ID, &m.Active, &takerSell,
   565  			&m.Taker, &m.TakerAcct, &takerAddr,
   566  			&m.Maker, &m.MakerAcct, &makerAddr,
   567  			&m.Epoch.Idx, &m.Epoch.Dur, &m.Quantity, &m.Rate,
   568  			&baseRate, &quoteRate, &status)
   569  	if err != nil {
   570  		return nil, err
   571  	}
   572  	m.TakerSell = takerSell.Bool
   573  	m.TakerAddr = takerAddr.String
   574  	m.MakerAddr = makerAddr.String
   575  	m.BaseRate = uint64(baseRate.Int64)
   576  	m.QuoteRate = uint64(quoteRate.Int64)
   577  	m.Status = order.MatchStatus(status)
   578  	return &m, nil
   579  }
   580  
   581  // matchStatusesByID retrieves the []*db.MatchStatus for the requested matchIDs.
   582  // See docs for MatchStatuses.
   583  func matchStatusesByID(ctx context.Context, dbe *sql.DB, aid account.AccountID, tableName string, matchIDs []order.MatchID) ([]*db.MatchStatus, error) {
   584  	stmt := fmt.Sprintf(internal.SelectMatchStatuses, tableName)
   585  	pqArr := make(pq.ByteaArray, 0, len(matchIDs))
   586  	for i := range matchIDs {
   587  		pqArr = append(pqArr, matchIDs[i][:])
   588  	}
   589  	rows, err := dbe.QueryContext(ctx, stmt, aid, pqArr)
   590  	if err != nil {
   591  		return nil, err
   592  	}
   593  	defer rows.Close()
   594  
   595  	statuses := make([]*db.MatchStatus, 0, len(matchIDs))
   596  	for rows.Next() {
   597  		status := new(db.MatchStatus)
   598  		err := rows.Scan(&status.TakerSell, &status.IsTaker, &status.IsMaker, &status.ID,
   599  			&status.Status, &status.MakerContract, &status.TakerContract, &status.MakerSwap,
   600  			&status.TakerSwap, &status.MakerRedeem, &status.TakerRedeem, &status.Secret, &status.Active)
   601  		if err != nil {
   602  			return nil, err
   603  		}
   604  		statuses = append(statuses, status)
   605  	}
   606  
   607  	if err := rows.Err(); err != nil {
   608  		return nil, err
   609  	}
   610  
   611  	return statuses, nil
   612  }
   613  
   614  // Swap Data
   615  //
   616  // In the swap process, the counterparties are:
   617  // - Initiator or party A on chain X. This is the maker in the DEX.
   618  // - Participant or party B on chain Y. This is the taker in the DEX.
   619  //
   620  // For each match, a successful swap will generate the following data that must
   621  // be stored:
   622  // - 5 client signatures. Both parties sign the data to acknowledge (1) the
   623  //   match ack, and (2) the counterparty's contract script and contract
   624  //   transaction. Plus, the taker acks the makers's redemption transaction.
   625  // - 2 swap contracts and the associated transaction outputs (more generally,
   626  //   coinIDs), one on each party's blockchain.
   627  // - the secret hash from the initiator contract
   628  // - the secret from from the initiator redeem
   629  // - 2 redemption transaction outputs (coinIDs).
   630  //
   631  // The methods for saving this data are defined below in the order in which the
   632  // data is expected from the parties.
   633  
   634  // SwapData retrieves the match status and all the SwapData for a match.
   635  func (a *Archiver) SwapData(mid db.MarketMatchID) (order.MatchStatus, *db.SwapData, error) {
   636  	marketSchema, err := a.marketSchema(mid.Base, mid.Quote)
   637  	if err != nil {
   638  		return 0, nil, err
   639  	}
   640  
   641  	matchesTableName := fullMatchesTableName(a.dbName, marketSchema)
   642  	stmt := fmt.Sprintf(internal.RetrieveSwapData, matchesTableName)
   643  
   644  	var sd db.SwapData
   645  	var status uint8
   646  	var contractATime, contractBTime, redeemATime, redeemBTime sql.NullInt64
   647  	err = a.db.QueryRow(stmt, mid).
   648  		Scan(&status,
   649  			&sd.SigMatchAckMaker, &sd.SigMatchAckTaker,
   650  			&sd.ContractACoinID, &sd.ContractA, &contractATime,
   651  			&sd.ContractAAckSig,
   652  			&sd.ContractBCoinID, &sd.ContractB, &contractBTime,
   653  			&sd.ContractBAckSig,
   654  			&sd.RedeemACoinID, &sd.RedeemASecret, &redeemATime,
   655  			&sd.RedeemAAckSig,
   656  			&sd.RedeemBCoinID, &redeemBTime)
   657  	if err != nil {
   658  		return 0, nil, err
   659  	}
   660  
   661  	sd.ContractATime = contractATime.Int64
   662  	sd.ContractBTime = contractBTime.Int64
   663  	sd.RedeemATime = redeemATime.Int64
   664  	sd.RedeemBTime = redeemBTime.Int64
   665  
   666  	return order.MatchStatus(status), &sd, nil
   667  }
   668  
   669  // updateMatchStmt executes a SQL statement with the provided arguments,
   670  // choosing the market's matches table from the MarketMatchID. Exactly 1 table
   671  // row must be updated, otherwise an error is returned.
   672  func (a *Archiver) updateMatchStmt(mid db.MarketMatchID, stmt string, args ...any) error {
   673  	marketSchema, err := a.marketSchema(mid.Base, mid.Quote)
   674  	if err != nil {
   675  		return err
   676  	}
   677  
   678  	matchesTableName := fullMatchesTableName(a.dbName, marketSchema)
   679  	stmt = fmt.Sprintf(stmt, matchesTableName)
   680  	N, err := sqlExec(a.db, stmt, args...)
   681  	if err != nil { // not just no rows updated
   682  		a.fatalBackendErr(err)
   683  		return err
   684  	}
   685  	if N != 1 {
   686  		return fmt.Errorf("updateMatchStmt: updated %d match rows for match %v, expected 1", N, mid)
   687  	}
   688  	return nil
   689  }
   690  
   691  // Match acknowledgement message signatures.
   692  
   693  // SaveMatchAckSigA records the match data acknowledgement signature from swap
   694  // party A (the initiator), which is the maker in the DEX.
   695  func (a *Archiver) SaveMatchAckSigA(mid db.MarketMatchID, sig []byte) error {
   696  	return a.updateMatchStmt(mid, internal.SetMakerMatchAckSig,
   697  		mid.MatchID, sig)
   698  }
   699  
   700  // SaveMatchAckSigB records the match data acknowledgement signature from swap
   701  // party B (the participant), which is the taker in the DEX.
   702  func (a *Archiver) SaveMatchAckSigB(mid db.MarketMatchID, sig []byte) error {
   703  	return a.updateMatchStmt(mid, internal.SetTakerMatchAckSig,
   704  		mid.MatchID, sig)
   705  }
   706  
   707  // Swap contracts, and counterparty audit acknowledgement signatures.
   708  
   709  // SaveContractA records party A's swap contract script and the coinID (e.g.
   710  // transaction output) containing the contract on chain X. Note that this
   711  // contract contains the secret hash.
   712  func (a *Archiver) SaveContractA(mid db.MarketMatchID, contract []byte, coinID []byte, timestamp int64) error {
   713  	return a.updateMatchStmt(mid, internal.SetInitiatorSwapData,
   714  		mid.MatchID, uint8(order.MakerSwapCast), coinID, contract, timestamp)
   715  }
   716  
   717  // SaveAuditAckSigB records party B's signature acknowledging their audit of A's
   718  // swap contract.
   719  func (a *Archiver) SaveAuditAckSigB(mid db.MarketMatchID, sig []byte) error {
   720  	return a.updateMatchStmt(mid, internal.SetParticipantContractAuditSig,
   721  		mid.MatchID, sig)
   722  }
   723  
   724  // SaveContractB records party B's swap contract script and the coinID (e.g.
   725  // transaction output) containing the contract on chain Y.
   726  func (a *Archiver) SaveContractB(mid db.MarketMatchID, contract []byte, coinID []byte, timestamp int64) error {
   727  	return a.updateMatchStmt(mid, internal.SetParticipantSwapData,
   728  		mid.MatchID, uint8(order.TakerSwapCast), coinID, contract, timestamp)
   729  }
   730  
   731  // SaveAuditAckSigA records party A's signature acknowledging their audit of B's
   732  // swap contract.
   733  func (a *Archiver) SaveAuditAckSigA(mid db.MarketMatchID, sig []byte) error {
   734  	return a.updateMatchStmt(mid, internal.SetInitiatorContractAuditSig,
   735  		mid.MatchID, sig)
   736  }
   737  
   738  // Redemption transactions, and counterparty acknowledgement signatures.
   739  
   740  // SaveRedeemA records party A's redemption coinID (e.g. transaction output),
   741  // which spends party B's swap contract on chain Y, and the secret revealed by
   742  // the signature script of the input spending the contract. Note that this
   743  // transaction will contain the secret, which party B extracts.
   744  func (a *Archiver) SaveRedeemA(mid db.MarketMatchID, coinID, secret []byte, timestamp int64) error {
   745  	return a.updateMatchStmt(mid, internal.SetInitiatorRedeemData,
   746  		mid.MatchID, uint8(order.MakerRedeemed), coinID, secret, timestamp)
   747  }
   748  
   749  // SaveRedeemAckSigB records party B's signature acknowledging party A's
   750  // redemption, which spent their swap contract on chain Y and revealed the
   751  // secret. Since this may be the final step in match negotiation, the match is
   752  // also flagged as inactive (not the same as archival or even status of
   753  // MatchComplete, which is set by SaveRedeemB) if the initiators's redeem ack
   754  // signature is already set.
   755  func (a *Archiver) SaveRedeemAckSigB(mid db.MarketMatchID, sig []byte) error {
   756  	return a.updateMatchStmt(mid, internal.SetParticipantRedeemAckSig,
   757  		mid.MatchID, sig)
   758  }
   759  
   760  // SaveRedeemB records party B's redemption coinID (e.g. transaction output),
   761  // which spends party A's swap contract on chain X.
   762  func (a *Archiver) SaveRedeemB(mid db.MarketMatchID, coinID []byte, timestamp int64) error {
   763  	return a.updateMatchStmt(mid, internal.SetParticipantRedeemData,
   764  		mid.MatchID, uint8(order.MatchComplete), coinID, timestamp)
   765  }
   766  
   767  // SetMatchInactive flags the match as done/inactive. This is not necessary if
   768  // SaveRedeemAckSigB is run for the match since it will flag the match as done.
   769  func (a *Archiver) SetMatchInactive(mid db.MarketMatchID, forgive bool) error {
   770  	if forgive {
   771  		return a.updateMatchStmt(mid, internal.SetSwapDoneForgiven, mid.MatchID)
   772  	} // else leave the forgiven column NULL
   773  	return a.updateMatchStmt(mid, internal.SetSwapDone, mid.MatchID)
   774  }