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, "eRate, &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, "eRate, &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, "eRate, &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, "eRate, &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, "eRate, &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, "eRate, &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 }