decred.org/dcrdex@v1.0.3/client/core/core_test.go (about) 1 //go:build !harness && !botlive 2 3 package core 4 5 import ( 6 "bytes" 7 "context" 8 crand "crypto/rand" 9 "crypto/sha256" 10 "encoding/hex" 11 "errors" 12 "fmt" 13 "math" 14 "math/rand" 15 "os" 16 "reflect" 17 "sort" 18 "strconv" 19 "strings" 20 "sync" 21 "sync/atomic" 22 "testing" 23 "time" 24 25 "decred.org/dcrdex/client/asset" 26 "decred.org/dcrdex/client/comms" 27 "decred.org/dcrdex/client/db" 28 dbtest "decred.org/dcrdex/client/db/test" 29 "decred.org/dcrdex/dex" 30 "decred.org/dcrdex/dex/calc" 31 "decred.org/dcrdex/dex/encode" 32 "decred.org/dcrdex/dex/encrypt" 33 "decred.org/dcrdex/dex/msgjson" 34 "decred.org/dcrdex/dex/order" 35 ordertest "decred.org/dcrdex/dex/order/test" 36 "decred.org/dcrdex/dex/wait" 37 "decred.org/dcrdex/server/account" 38 serverdex "decred.org/dcrdex/server/dex" 39 "github.com/decred/dcrd/crypto/blake256" 40 "github.com/decred/dcrd/dcrec/secp256k1/v4" 41 "golang.org/x/text/language" 42 "golang.org/x/text/message" 43 ) 44 45 func init() { 46 asset.Register(tUTXOAssetA.ID, &tDriver{ 47 decodedCoinID: tUTXOAssetA.Symbol + "-coin", 48 winfo: tWalletInfo, 49 }) 50 asset.Register(tUTXOAssetB.ID, &tCreator{ 51 tDriver: &tDriver{ 52 decodedCoinID: tUTXOAssetB.Symbol + "-coin", 53 winfo: tWalletInfo, 54 }, 55 }) 56 asset.Register(tACCTAsset.ID, &tCreator{ 57 tDriver: &tDriver{ 58 decodedCoinID: tACCTAsset.Symbol + "-coin", 59 winfo: tWalletInfo, 60 }, 61 }) 62 rand.Seed(time.Now().UnixNano()) 63 } 64 65 var ( 66 tCtx context.Context 67 dcrBtcLotSize uint64 = 1e7 68 dcrBtcRateStep uint64 = 10 69 tUTXOAssetA = &dex.Asset{ 70 ID: 42, 71 Symbol: "dcr", 72 Version: 0, // match the stubbed (*TXCWallet).Info result 73 MaxFeeRate: 10, 74 SwapConf: 1, 75 } 76 tSwapSizeA uint64 = 251 77 78 tUTXOAssetB = &dex.Asset{ 79 ID: 0, 80 Symbol: "btc", 81 Version: 0, // match the stubbed (*TXCWallet).Info result 82 MaxFeeRate: 2, 83 SwapConf: 1, 84 } 85 tSwapSizeB uint64 = 225 86 87 tACCTAsset = &dex.Asset{ 88 ID: 60, 89 Symbol: "eth", 90 Version: 0, // match the stubbed (*TXCWallet).Info result 91 MaxFeeRate: 20, 92 SwapConf: 1, 93 } 94 tDexPriv *secp256k1.PrivateKey 95 tDexKey *secp256k1.PublicKey 96 tPW = []byte("dexpw") 97 wPW = []byte("walletpw") 98 tDexHost = "somedex.tld:7232" 99 tDcrBtcMktName = "dcr_btc" 100 tBtcEthMktName = "btc_eth" 101 tErr = fmt.Errorf("test error") 102 tFee uint64 = 1e8 103 tFeeAsset uint32 = 42 104 tUnparseableHost = string([]byte{0x7f}) 105 tSwapFeesPaid uint64 = 500 106 tRedemptionFeesPaid uint64 = 350 107 tLogger = dex.StdOutLogger("TCORE", dex.LevelInfo) 108 tMaxFeeRate uint64 = 10 109 tWalletInfo = &asset.WalletInfo{ 110 SupportedVersions: []uint32{0}, 111 UnitInfo: dex.UnitInfo{ 112 Conventional: dex.Denomination{ 113 ConversionFactor: 1e8, 114 }, 115 }, 116 AvailableWallets: []*asset.WalletDefinition{{ 117 Type: "type", 118 }}, 119 } 120 dcrBondAsset = &msgjson.BondAsset{ID: 42, Amt: tFee, Confs: 1} 121 ) 122 123 type tMsg = *msgjson.Message 124 type msgFunc = func(*msgjson.Message) 125 126 func uncovertAssetInfo(ai *dex.Asset) *msgjson.Asset { 127 return &msgjson.Asset{ 128 Symbol: ai.Symbol, 129 ID: ai.ID, 130 Version: ai.Version, 131 MaxFeeRate: ai.MaxFeeRate, 132 SwapConf: uint16(ai.SwapConf), 133 } 134 } 135 136 func makeAcker(serializer func(msg *msgjson.Message) msgjson.Signable) func(msg *msgjson.Message, f msgFunc) error { 137 return func(msg *msgjson.Message, f msgFunc) error { 138 signable := serializer(msg) 139 sigMsg := signable.Serialize() 140 sig := signMsg(tDexPriv, sigMsg) 141 ack := &msgjson.Acknowledgement{ 142 Sig: sig, 143 } 144 resp, _ := msgjson.NewResponse(msg.ID, ack, nil) 145 f(resp) 146 return nil 147 } 148 } 149 150 var ( 151 invalidAcker = func(msg *msgjson.Message, f msgFunc) error { 152 resp, _ := msgjson.NewResponse(msg.ID, msg, nil) 153 f(resp) 154 return nil 155 } 156 initAcker = makeAcker(func(msg *msgjson.Message) msgjson.Signable { 157 init := new(msgjson.Init) 158 msg.Unmarshal(init) 159 return init 160 }) 161 redeemAcker = makeAcker(func(msg *msgjson.Message) msgjson.Signable { 162 redeem := new(msgjson.Redeem) 163 msg.Unmarshal(redeem) 164 return redeem 165 }) 166 ) 167 168 type TWebsocket struct { 169 mtx sync.RWMutex 170 id uint64 171 sendErr error 172 sendMsgErrChan chan *msgjson.Error 173 reqErr error 174 connectErr error 175 msgs <-chan *msgjson.Message 176 // handlers simulates a peer (server) response for request, and handles the 177 // response with the msgFunc. 178 handlers map[string][]func(*msgjson.Message, msgFunc) error 179 submittedBond *msgjson.PostBond 180 liveBondExpiry uint64 181 } 182 183 func newTWebsocket() *TWebsocket { 184 return &TWebsocket{ 185 msgs: make(<-chan *msgjson.Message), 186 handlers: make(map[string][]func(*msgjson.Message, msgFunc) error), 187 } 188 } 189 190 func tNewAccount(crypter *tCrypter) *dexAccount { 191 privKey, _ := secp256k1.GeneratePrivateKey() 192 encKey, err := crypter.Encrypt(privKey.Serialize()) 193 if err != nil { 194 panic(err) 195 } 196 return &dexAccount{ 197 host: tDexHost, 198 encKey: encKey, 199 dexPubKey: tDexKey, 200 privKey: privKey, 201 id: account.NewID(privKey.PubKey().SerializeCompressed()), 202 // feeAssetID is 0 (btc) 203 // tier, bonds, etc. set on auth 204 pendingBondsConfs: make(map[string]uint32), 205 rep: account.Reputation{BondedTier: 1}, // not suspended by default 206 } 207 } 208 209 func testDexConnection(ctx context.Context, crypter *tCrypter) (*dexConnection, *TWebsocket, *dexAccount) { 210 conn := newTWebsocket() 211 connMaster := dex.NewConnectionMaster(conn) 212 connMaster.Connect(ctx) 213 acct := tNewAccount(crypter) 214 return &dexConnection{ 215 WsConn: conn, 216 log: tLogger, 217 connMaster: connMaster, 218 ticker: newDexTicker(time.Millisecond * 1000 / 3), 219 acct: acct, 220 assets: map[uint32]*dex.Asset{ 221 tUTXOAssetA.ID: tUTXOAssetA, 222 tUTXOAssetB.ID: tUTXOAssetB, 223 tACCTAsset.ID: tACCTAsset, 224 }, 225 books: make(map[string]*bookie), 226 cfg: &msgjson.ConfigResult{ 227 APIVersion: serverdex.V1APIVersion, 228 DEXPubKey: acct.dexPubKey.SerializeCompressed(), 229 CancelMax: 0.8, 230 BroadcastTimeout: 1000, // 1000 ms for faster expiration, but ticker fires fast 231 Assets: []*msgjson.Asset{ 232 uncovertAssetInfo(tUTXOAssetA), 233 uncovertAssetInfo(tUTXOAssetB), 234 uncovertAssetInfo(tACCTAsset), 235 }, 236 Markets: []*msgjson.Market{ 237 { 238 Name: tDcrBtcMktName, 239 Base: tUTXOAssetA.ID, 240 Quote: tUTXOAssetB.ID, 241 LotSize: dcrBtcLotSize, 242 ParcelSize: 1, 243 RateStep: dcrBtcRateStep, 244 EpochLen: 60000, 245 MarketBuyBuffer: 1.1, 246 MarketStatus: msgjson.MarketStatus{ 247 StartEpoch: 12, // since the stone age 248 FinalEpoch: 0, // no scheduled suspend 249 // Persist: nil, 250 }, 251 }, 252 { 253 Name: tBtcEthMktName, 254 Base: tUTXOAssetB.ID, 255 Quote: tACCTAsset.ID, 256 LotSize: dcrBtcLotSize, 257 RateStep: dcrBtcRateStep, 258 EpochLen: 60000, 259 MarketBuyBuffer: 1.1, 260 MarketStatus: msgjson.MarketStatus{ 261 StartEpoch: 12, 262 FinalEpoch: 0, 263 }, 264 }, 265 }, 266 BondExpiry: 86400, // >0 make client treat as API v1 267 BondAssets: map[string]*msgjson.BondAsset{ 268 "dcr": dcrBondAsset, 269 }, 270 BinSizes: []string{"1h", "24h"}, 271 }, 272 notify: func(Notification) {}, 273 trades: make(map[order.OrderID]*trackedTrade), 274 cancels: make(map[order.OrderID]order.OrderID), 275 inFlightOrders: make(map[uint64]*InFlightOrder), 276 epoch: map[string]uint64{tDcrBtcMktName: 0}, 277 resolvedEpoch: map[string]uint64{tDcrBtcMktName: 0}, 278 apiVer: serverdex.PreAPIVersion, 279 connectionStatus: uint32(comms.Connected), 280 reportingConnects: 1, 281 spots: make(map[string]*msgjson.Spot), 282 }, conn, acct 283 } 284 285 func (conn *TWebsocket) queueResponse(route string, handler func(*msgjson.Message, msgFunc) error) { 286 conn.mtx.Lock() 287 defer conn.mtx.Unlock() 288 handlers := conn.handlers[route] 289 if handlers == nil { 290 handlers = make([]func(*msgjson.Message, msgFunc) error, 0, 1) 291 } 292 conn.handlers[route] = append(handlers, handler) // NOTE: handler is called by RequestWithTimeout 293 } 294 295 func (conn *TWebsocket) NextID() uint64 { 296 conn.mtx.Lock() 297 defer conn.mtx.Unlock() 298 conn.id++ 299 return conn.id 300 } 301 func (conn *TWebsocket) Send(msg *msgjson.Message) error { 302 if conn.sendMsgErrChan != nil { 303 resp, err := msg.Response() 304 if err != nil { 305 return err 306 } 307 if resp.Error != nil { 308 conn.sendMsgErrChan <- resp.Error 309 return nil // the response was sent successfully 310 } 311 } 312 313 return conn.sendErr 314 } 315 316 func (conn *TWebsocket) SendRaw([]byte) error { 317 return conn.sendErr 318 } 319 func (conn *TWebsocket) Request(msg *msgjson.Message, f msgFunc) error { 320 return conn.RequestWithTimeout(msg, f, 0, func() {}) 321 } 322 func (conn *TWebsocket) RequestRaw(msgID uint64, rawMsg []byte, respHandler func(*msgjson.Message)) error { 323 return nil 324 } 325 func (conn *TWebsocket) RequestWithTimeout(msg *msgjson.Message, f func(*msgjson.Message), _ time.Duration, _ func()) error { 326 if conn.reqErr != nil { 327 return conn.reqErr 328 } 329 conn.mtx.Lock() 330 defer conn.mtx.Unlock() 331 handlers := conn.handlers[msg.Route] 332 if len(handlers) > 0 { 333 handler := handlers[0] 334 conn.handlers[msg.Route] = handlers[1:] 335 return handler(msg, f) 336 } 337 return fmt.Errorf("no handler for route %q", msg.Route) 338 } 339 func (conn *TWebsocket) MessageSource() <-chan *msgjson.Message { return conn.msgs } // use when Core.listen is running 340 func (conn *TWebsocket) IsDown() bool { 341 return false 342 } 343 func (conn *TWebsocket) Connect(context.Context) (*sync.WaitGroup, error) { 344 // NOTE: tCore's wsConstructor just returns a reused conn, so we can't close 345 // conn.msgs on ctx cancel. See the wsConstructor definition in newTestRig. 346 // Consider reworking the tests (TODO). 347 return &sync.WaitGroup{}, conn.connectErr 348 } 349 350 func (conn *TWebsocket) UpdateURL(string) {} 351 352 type TDB struct { 353 updateWalletErr error 354 acct *db.AccountInfo 355 acctErr error 356 createAccountErr error 357 addBondErr error 358 updateOrderErr error 359 activeDEXOrders []*db.MetaOrder 360 matchesForOID []*db.MetaMatch 361 matchesForOIDErr error 362 updateMatchChan chan order.MatchStatus 363 activeMatchOIDs []order.OrderID 364 activeMatchOIDSErr error 365 lastStatusID order.OrderID 366 lastStatus order.OrderStatus 367 wallet *db.Wallet 368 walletErr error 369 setWalletPwErr error 370 orderOrders map[order.OrderID]*db.MetaOrder 371 orderErr error 372 linkedFromID order.OrderID 373 linkedToID order.OrderID 374 existValues map[string]bool 375 accountProofErr error 376 verifyCreateAccount bool 377 verifyUpdateAccountInfo bool 378 disabledHost *string 379 disableAccountErr error 380 creds *db.PrimaryCredentials 381 setCredsErr error 382 legacyKeyErr error 383 recryptErr error 384 deleteInactiveOrdersErr error 385 archivedOrders int 386 deleteInactiveMatchesErr error 387 archivedMatches int 388 updateAccountInfoErr error 389 } 390 391 func (tdb *TDB) Run(context.Context) {} 392 393 func (tdb *TDB) ListAccounts() ([]string, error) { 394 return nil, nil 395 } 396 397 func (tdb *TDB) Accounts() ([]*db.AccountInfo, error) { 398 return []*db.AccountInfo{}, nil 399 } 400 401 func (tdb *TDB) Account(url string) (*db.AccountInfo, error) { 402 return tdb.acct, tdb.acctErr 403 } 404 405 func (tdb *TDB) CreateAccount(ai *db.AccountInfo) error { 406 tdb.verifyCreateAccount = true 407 tdb.acct = ai 408 return tdb.createAccountErr 409 } 410 411 func (tdb *TDB) NextBondKeyIndex(assetID uint32) (uint32, error) { 412 return 0, nil 413 } 414 415 func (tdb *TDB) AddBond(host string, bond *db.Bond) error { 416 return tdb.addBondErr 417 } 418 419 func (tdb *TDB) ConfirmBond(host string, assetID uint32, bondCoinID []byte) error { 420 return nil 421 } 422 func (tdb *TDB) BondRefunded(host string, assetID uint32, bondCoinID []byte) error { 423 return nil 424 } 425 426 func (tdb *TDB) ToggleAccountStatus(host string, disable bool) error { 427 if disable { 428 tdb.disabledHost = &host 429 } else { 430 tdb.disabledHost = nil 431 } 432 return tdb.disableAccountErr 433 } 434 435 func (tdb *TDB) UpdateAccountInfo(ai *db.AccountInfo) error { 436 tdb.verifyUpdateAccountInfo = true 437 tdb.acct = ai 438 return tdb.updateAccountInfoErr 439 } 440 441 func (tdb *TDB) UpdateOrder(m *db.MetaOrder) error { 442 return tdb.updateOrderErr 443 } 444 445 func (tdb *TDB) ActiveDEXOrders(dex string) ([]*db.MetaOrder, error) { 446 return tdb.activeDEXOrders, nil 447 } 448 449 func (tdb *TDB) ActiveOrders() ([]*db.MetaOrder, error) { 450 return nil, nil 451 } 452 453 func (tdb *TDB) AccountOrders(dex string, n int, since uint64) ([]*db.MetaOrder, error) { 454 return nil, nil 455 } 456 457 func (tdb *TDB) Order(oid order.OrderID) (*db.MetaOrder, error) { 458 if tdb.orderErr != nil { 459 return nil, tdb.orderErr 460 } 461 return tdb.orderOrders[oid], nil 462 } 463 464 func (tdb *TDB) Orders(*db.OrderFilter) ([]*db.MetaOrder, error) { 465 return nil, nil 466 } 467 468 func (tdb *TDB) MarketOrders(dex string, base, quote uint32, n int, since uint64) ([]*db.MetaOrder, error) { 469 return nil, nil 470 } 471 472 func (tdb *TDB) UpdateOrderMetaData(order.OrderID, *db.OrderMetaData) error { 473 return nil 474 } 475 476 func (tdb *TDB) UpdateOrderStatus(oid order.OrderID, status order.OrderStatus) error { 477 tdb.lastStatusID = oid 478 tdb.lastStatus = status 479 return nil 480 } 481 482 func (tdb *TDB) LinkOrder(oid, linkedID order.OrderID) error { 483 tdb.linkedFromID = oid 484 tdb.linkedToID = linkedID 485 return nil 486 } 487 488 func (tdb *TDB) UpdateMatch(m *db.MetaMatch) error { 489 if tdb.updateMatchChan != nil { 490 tdb.updateMatchChan <- m.Status 491 } 492 return nil 493 } 494 495 func (tdb *TDB) ActiveMatches() ([]*db.MetaMatch, error) { 496 return nil, nil 497 } 498 499 func (tdb *TDB) MatchesForOrder(oid order.OrderID, excludeCancels bool) ([]*db.MetaMatch, error) { 500 return tdb.matchesForOID, tdb.matchesForOIDErr 501 } 502 503 func (tdb *TDB) DEXOrdersWithActiveMatches(dex string) ([]order.OrderID, error) { 504 return tdb.activeMatchOIDs, tdb.activeMatchOIDSErr 505 } 506 507 func (tdb *TDB) UpdateWallet(wallet *db.Wallet) error { 508 tdb.wallet = wallet 509 return tdb.updateWalletErr 510 } 511 512 func (tdb *TDB) SetWalletPassword(wid []byte, newPW []byte) error { 513 return tdb.setWalletPwErr 514 } 515 516 func (tdb *TDB) UpdateBalance(wid []byte, balance *db.Balance) error { 517 return nil 518 } 519 520 func (tdb *TDB) UpdateWalletStatus(wid []byte, disable bool) error { 521 return nil 522 } 523 524 func (tdb *TDB) Wallets() ([]*db.Wallet, error) { 525 return nil, nil 526 } 527 528 func (tdb *TDB) Wallet([]byte) (*db.Wallet, error) { 529 return tdb.wallet, tdb.walletErr 530 } 531 532 func (tdb *TDB) SaveNotification(*db.Notification) error { return nil } 533 func (tdb *TDB) BackupTo(dst string, overwrite, compact bool) error { return nil } 534 func (tdb *TDB) NotificationsN(int) ([]*db.Notification, error) { return nil, nil } 535 func (tdb *TDB) SavePokes([]*db.Notification) error { return nil } 536 func (tdb *TDB) LoadPokes() ([]*db.Notification, error) { return nil, nil } 537 538 func (tdb *TDB) SetPrimaryCredentials(creds *db.PrimaryCredentials) error { 539 if tdb.setCredsErr != nil { 540 return tdb.setCredsErr 541 } 542 tdb.creds = creds 543 return nil 544 } 545 546 func (tdb *TDB) DeleteInactiveOrders(ctx context.Context, olderThan *time.Time, perBatchFn func(ords *db.MetaOrder) error) (int, error) { 547 return tdb.archivedOrders, tdb.deleteInactiveOrdersErr 548 } 549 550 func (tdb *TDB) DeleteInactiveMatches(ctx context.Context, olderThan *time.Time, perBatchFn func(mtchs *db.MetaMatch, isSell bool) error) (int, error) { 551 return tdb.archivedMatches, tdb.deleteInactiveMatchesErr 552 } 553 554 func (tdb *TDB) PrimaryCredentials() (*db.PrimaryCredentials, error) { 555 return tdb.creds, nil 556 } 557 func (tdb *TDB) SetSeedGenerationTime(time uint64) error { 558 return nil 559 } 560 func (tdb *TDB) SeedGenerationTime() (uint64, error) { 561 return 0, nil 562 } 563 func (tdb *TDB) DisabledRateSources() ([]string, error) { 564 return nil, nil 565 } 566 func (tdb *TDB) SaveDisabledRateSources(disableSources []string) error { 567 return nil 568 } 569 func (tdb *TDB) Recrypt(creds *db.PrimaryCredentials, oldCrypter, newCrypter encrypt.Crypter) ( 570 walletUpdates map[uint32][]byte, acctUpdates map[string][]byte, err error) { 571 572 if tdb.recryptErr != nil { 573 return nil, nil, tdb.recryptErr 574 } 575 576 return nil, nil, nil 577 } 578 579 func (tdb *TDB) Backup() error { 580 return nil 581 } 582 583 func (tdb *TDB) AckNotification(id []byte) error { return nil } 584 585 func (tdb *TDB) SetLanguage(lang string) error { 586 return nil 587 } 588 func (tdb *TDB) Language() (string, error) { 589 return "en-US", nil 590 } 591 592 type tCoin struct { 593 id []byte 594 595 val uint64 596 } 597 598 func (c *tCoin) ID() dex.Bytes { 599 return c.id 600 } 601 602 func (c *tCoin) TxID() string { 603 return "" 604 } 605 606 func (c *tCoin) String() string { 607 return hex.EncodeToString(c.id) 608 } 609 610 func (c *tCoin) Value() uint64 { 611 return c.val 612 } 613 614 type tReceipt struct { 615 coin *tCoin 616 contract []byte 617 expiration time.Time 618 } 619 620 func (r *tReceipt) Coin() asset.Coin { 621 return r.coin 622 } 623 624 func (r *tReceipt) Contract() dex.Bytes { 625 return r.contract 626 } 627 628 func (r *tReceipt) Expiration() time.Time { 629 return r.expiration 630 } 631 632 func (r *tReceipt) String() string { 633 return r.coin.String() 634 } 635 636 func (r *tReceipt) SignedRefund() dex.Bytes { 637 return nil 638 } 639 640 type TXCWallet struct { 641 swapSize uint64 642 sendFeeSuggestion uint64 643 sendCoin *tCoin 644 sendErr error 645 addrErr error 646 signCoinErr error 647 lastSwaps []*asset.Swaps 648 lastRedeems []*asset.RedeemForm 649 swapReceipts []asset.Receipt 650 swapCounter int 651 swapErr error 652 auditInfo *asset.AuditInfo 653 auditErr error 654 auditChan chan struct{} 655 refundCoin dex.Bytes 656 refundErr error 657 refundFeeSuggestion uint64 658 redeemCoins []dex.Bytes 659 redeemCounter int 660 redeemFeeSuggestion uint64 661 redeemErr error 662 redeemErrChan chan error 663 badSecret bool 664 fundedVal uint64 665 fundedSwaps uint64 666 connectErr error 667 unlockErr error 668 balErr error 669 bal *asset.Balance 670 fundingMtx sync.RWMutex 671 fundingCoins asset.Coins 672 fundRedeemScripts []dex.Bytes 673 returnedCoins asset.Coins 674 fundingCoinErr error 675 lockErr error 676 locked bool 677 changeCoin *tCoin 678 syncStatus func() (bool, float32, error) 679 confsMtx sync.RWMutex 680 confs map[string]uint32 681 confsErr map[string]error 682 preSwapForm *asset.PreSwapForm 683 preSwap *asset.PreSwap 684 preRedeemForm *asset.PreRedeemForm 685 preRedeem *asset.PreRedeem 686 ownsAddress bool 687 ownsAddressErr error 688 pubKeys []dex.Bytes 689 sigs []dex.Bytes 690 feeCoin []byte 691 makeRegFeeTxErr error 692 feeCoinSent []byte 693 sendTxnErr error 694 contractExpired bool 695 contractLockTime time.Time 696 accelerationParams *struct { 697 swapCoins []dex.Bytes 698 accelerationCoins []dex.Bytes 699 changeCoin dex.Bytes 700 feeSuggestion uint64 701 newFeeRate uint64 702 requiredForRemainingSwaps uint64 703 } 704 newAccelerationTxID string 705 newChangeCoinID *dex.Bytes 706 preAccelerateSwapRate uint64 707 preAccelerateSuggestedRange asset.XYRange 708 accelerationEstimate uint64 709 accelerateOrderErr error 710 info *asset.WalletInfo 711 bondTxCoinID []byte 712 refundBondCoin asset.Coin 713 refundBondErr error 714 makeBondTxErr error 715 reserves atomic.Uint64 716 findBond *asset.BondDetails 717 findBondErr error 718 719 confirmRedemptionResult *asset.ConfirmRedemptionStatus 720 confirmRedemptionErr error 721 confirmRedemptionCalled bool 722 723 estFee uint64 724 estFeeErr error 725 validAddr bool 726 727 returnedAddr string 728 returnedContracts [][]byte 729 } 730 731 var _ asset.Accelerator = (*TXCWallet)(nil) 732 var _ asset.Withdrawer = (*TXCWallet)(nil) 733 734 func newTWallet(assetID uint32) (*xcWallet, *TXCWallet) { 735 w := &TXCWallet{ 736 changeCoin: &tCoin{id: encode.RandomBytes(36)}, 737 syncStatus: func() (synced bool, progress float32, err error) { return true, 1, nil }, 738 confs: make(map[string]uint32), 739 confsErr: make(map[string]error), 740 ownsAddress: true, 741 contractLockTime: time.Now().Add(time.Minute), 742 lastSwaps: make([]*asset.Swaps, 0), 743 lastRedeems: make([]*asset.RedeemForm, 0), 744 info: &asset.WalletInfo{ 745 SupportedVersions: []uint32{0}, 746 }, 747 bondTxCoinID: encode.RandomBytes(32), 748 } 749 var broadcasting uint32 = 1 750 xcWallet := &xcWallet{ 751 log: tLogger, 752 supportedVersions: w.info.SupportedVersions, 753 Wallet: w, 754 Symbol: dex.BipIDSymbol(assetID), 755 connector: dex.NewConnectionMaster(w), 756 AssetID: assetID, 757 hookedUp: true, 758 dbID: encode.Uint32Bytes(assetID), 759 encPass: []byte{0x01}, 760 peerCount: 1, 761 syncStatus: &asset.SyncStatus{Synced: true}, 762 pw: tPW, 763 traits: asset.DetermineWalletTraits(w), 764 broadcasting: &broadcasting, 765 } 766 767 return xcWallet, w 768 } 769 770 func (w *TXCWallet) Info() *asset.WalletInfo { 771 return w.info 772 } 773 774 func (w *TXCWallet) OwnsDepositAddress(address string) (bool, error) { 775 return w.ownsAddress, w.ownsAddressErr 776 } 777 778 func (w *TXCWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) { 779 var wg sync.WaitGroup 780 wg.Add(1) 781 go func() { 782 <-ctx.Done() 783 wg.Done() 784 }() 785 return &wg, w.connectErr 786 } 787 788 func (w *TXCWallet) Balance() (*asset.Balance, error) { 789 if w.balErr != nil { 790 return nil, w.balErr 791 } 792 if w.bal == nil { 793 w.bal = new(asset.Balance) 794 } 795 return w.bal, nil 796 } 797 798 func (w *TXCWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, feeSuggestion uint64) (*asset.ConfirmRedemptionStatus, error) { 799 w.confirmRedemptionCalled = true 800 return w.confirmRedemptionResult, w.confirmRedemptionErr 801 } 802 803 func (w *TXCWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uint64, error) { 804 w.fundedVal = ord.Value 805 w.fundedSwaps = ord.MaxSwapCount 806 return w.fundingCoins, w.fundRedeemScripts, 0, w.fundingCoinErr 807 } 808 809 func (w *TXCWallet) MaxOrder(*asset.MaxOrderForm) (*asset.SwapEstimate, error) { 810 return nil, nil 811 } 812 813 func (w *TXCWallet) PreSwap(form *asset.PreSwapForm) (*asset.PreSwap, error) { 814 w.preSwapForm = form 815 return w.preSwap, nil 816 } 817 818 func (w *TXCWallet) PreRedeem(form *asset.PreRedeemForm) (*asset.PreRedeem, error) { 819 w.preRedeemForm = form 820 return w.preRedeem, nil 821 } 822 func (w *TXCWallet) RedemptionFees() (uint64, error) { return 0, nil } 823 824 func (w *TXCWallet) ReturnCoins(coins asset.Coins) error { 825 w.fundingMtx.Lock() 826 defer w.fundingMtx.Unlock() 827 w.returnedCoins = coins 828 coinInSlice := func(coin asset.Coin) bool { 829 for _, c := range coins { 830 if bytes.Equal(c.ID(), coin.ID()) { 831 return true 832 } 833 } 834 return false 835 } 836 837 for _, c := range w.fundingCoins { 838 if coinInSlice(c) { 839 continue 840 } 841 return errors.New("not found") 842 } 843 return nil 844 } 845 846 func (w *TXCWallet) FundingCoins([]dex.Bytes) (asset.Coins, error) { 847 return w.fundingCoins, w.fundingCoinErr 848 } 849 850 func (w *TXCWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint64, error) { 851 w.swapCounter++ 852 w.lastSwaps = append(w.lastSwaps, swaps) 853 if w.swapErr != nil { 854 return nil, nil, 0, w.swapErr 855 } 856 return w.swapReceipts, w.changeCoin, tSwapFeesPaid, nil 857 } 858 859 func (w *TXCWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, uint64, error) { 860 w.redeemFeeSuggestion = form.FeeSuggestion 861 defer func() { 862 if w.redeemErrChan != nil { 863 w.redeemErrChan <- w.redeemErr 864 } 865 }() 866 w.lastRedeems = append(w.lastRedeems, form) 867 w.redeemCounter++ 868 if w.redeemErr != nil { 869 return nil, nil, 0, w.redeemErr 870 } 871 return w.redeemCoins, &tCoin{id: []byte{0x0c, 0x0d}}, tRedemptionFeesPaid, nil 872 } 873 874 func (w *TXCWallet) SignMessage(asset.Coin, dex.Bytes) (pubkeys, sigs []dex.Bytes, err error) { 875 return w.pubKeys, w.sigs, w.signCoinErr 876 } 877 878 func (w *TXCWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroadcast bool) (*asset.AuditInfo, error) { 879 defer func() { 880 if w.auditChan != nil { 881 w.auditChan <- struct{}{} 882 } 883 }() 884 return w.auditInfo, w.auditErr 885 } 886 887 func (w *TXCWallet) LockTimeExpired(_ context.Context, lockTime time.Time) (bool, error) { 888 return w.contractExpired, nil 889 } 890 891 func (w *TXCWallet) ContractLockTimeExpired(_ context.Context, contract dex.Bytes) (bool, time.Time, error) { 892 return w.contractExpired, w.contractLockTime, nil 893 } 894 895 func (w *TXCWallet) FindRedemption(ctx context.Context, coinID, _ dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) { 896 return nil, nil, fmt.Errorf("not mocked") 897 } 898 899 func (w *TXCWallet) Refund(refundCoin dex.Bytes, refundContract dex.Bytes, feeSuggestion uint64) (dex.Bytes, error) { 900 w.refundFeeSuggestion = feeSuggestion 901 return w.refundCoin, w.refundErr 902 } 903 904 func (w *TXCWallet) DepositAddress() (string, error) { 905 return "", w.addrErr 906 } 907 908 func (w *TXCWallet) RedemptionAddress() (string, error) { 909 return "", w.addrErr 910 } 911 912 func (w *TXCWallet) NewAddress() (string, error) { 913 return "", w.addrErr 914 } 915 916 func (w *TXCWallet) Unlock(pw []byte) error { 917 return w.unlockErr 918 } 919 920 func (w *TXCWallet) Lock() error { 921 return w.lockErr 922 } 923 924 func (w *TXCWallet) Locked() bool { 925 return w.locked 926 } 927 928 func (w *TXCWallet) ConfirmTime(id dex.Bytes, nConfs uint32) (time.Time, error) { 929 return time.Time{}, nil 930 } 931 932 func (w *TXCWallet) Send(address string, value, feeSuggestion uint64) (asset.Coin, error) { 933 w.sendFeeSuggestion = feeSuggestion 934 w.sendCoin.val = value 935 return w.sendCoin, w.sendErr 936 } 937 938 func (w *TXCWallet) SendTransaction(rawTx []byte) ([]byte, error) { 939 return w.feeCoinSent, w.sendTxnErr 940 } 941 942 func (w *TXCWallet) Withdraw(address string, value, feeSuggestion uint64) (asset.Coin, error) { 943 w.sendFeeSuggestion = feeSuggestion 944 return w.sendCoin, w.sendErr 945 } 946 947 func (w *TXCWallet) ValidateAddress(address string) bool { 948 return w.validAddr 949 } 950 951 func (w *TXCWallet) EstimateSendTxFee(address string, value, feeRate uint64, subtract, maxWithdraw bool) (fee uint64, isValidAddress bool, err error) { 952 return w.estFee, true, w.estFeeErr 953 } 954 955 func (w *TXCWallet) ValidateSecret(secret, secretHash []byte) bool { 956 return !w.badSecret 957 } 958 959 func (w *TXCWallet) SyncStatus() (*asset.SyncStatus, error) { 960 synced, progress, err := w.syncStatus() 961 if err != nil { 962 return nil, err 963 } 964 blocks := uint64(math.Round(float64(progress) * 100)) 965 return &asset.SyncStatus{Synced: synced, TargetHeight: blocks, Blocks: blocks}, nil 966 } 967 968 func (w *TXCWallet) setConfs(coinID dex.Bytes, confs uint32, err error) { 969 id := coinID.String() 970 w.confsMtx.Lock() 971 w.confs[id] = confs 972 w.confsErr[id] = err 973 w.confsMtx.Unlock() 974 } 975 976 func (w *TXCWallet) tConfirmations(_ context.Context, coinID dex.Bytes) (uint32, error) { 977 id := coinID.String() 978 w.confsMtx.RLock() 979 defer w.confsMtx.RUnlock() 980 return w.confs[id], w.confsErr[id] 981 } 982 983 func (w *TXCWallet) SwapConfirmations(ctx context.Context, coinID dex.Bytes, contract dex.Bytes, matchTime time.Time) (uint32, bool, error) { 984 confs, err := w.tConfirmations(ctx, coinID) 985 return confs, false, err 986 } 987 988 func (w *TXCWallet) RegFeeConfirmations(ctx context.Context, coinID dex.Bytes) (uint32, error) { 989 return w.tConfirmations(ctx, coinID) 990 } 991 992 func (w *TXCWallet) FeesForRemainingSwaps(n, feeRate uint64) uint64 { 993 return n * feeRate * w.swapSize 994 } 995 func (w *TXCWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) { 996 if w.accelerateOrderErr != nil { 997 return nil, "", w.accelerateOrderErr 998 } 999 1000 w.accelerationParams = &struct { 1001 swapCoins []dex.Bytes 1002 accelerationCoins []dex.Bytes 1003 changeCoin dex.Bytes 1004 feeSuggestion uint64 1005 newFeeRate uint64 1006 requiredForRemainingSwaps uint64 1007 }{ 1008 swapCoins: swapCoins, 1009 accelerationCoins: accelerationCoins, 1010 changeCoin: changeCoin, 1011 requiredForRemainingSwaps: requiredForRemainingSwaps, 1012 newFeeRate: newFeeRate, 1013 } 1014 if w.newChangeCoinID != nil { 1015 return &tCoin{id: *w.newChangeCoinID}, w.newAccelerationTxID, nil 1016 } 1017 1018 return nil, w.newAccelerationTxID, nil 1019 } 1020 1021 func (w *TXCWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, *asset.XYRange, *asset.EarlyAcceleration, error) { 1022 if w.accelerateOrderErr != nil { 1023 return 0, nil, nil, w.accelerateOrderErr 1024 } 1025 1026 w.accelerationParams = &struct { 1027 swapCoins []dex.Bytes 1028 accelerationCoins []dex.Bytes 1029 changeCoin dex.Bytes 1030 feeSuggestion uint64 1031 newFeeRate uint64 1032 requiredForRemainingSwaps uint64 1033 }{ 1034 swapCoins: swapCoins, 1035 accelerationCoins: accelerationCoins, 1036 changeCoin: changeCoin, 1037 requiredForRemainingSwaps: requiredForRemainingSwaps, 1038 feeSuggestion: feeSuggestion, 1039 } 1040 1041 return w.preAccelerateSwapRate, &w.preAccelerateSuggestedRange, nil, nil 1042 } 1043 1044 func (w *TXCWallet) SingleLotSwapRefundFees(version uint32, feeRate uint64, useSafeTxSize bool) (uint64, uint64, error) { 1045 return 0, 0, nil 1046 } 1047 1048 func (w *TXCWallet) SingleLotRedeemFees(version uint32, feeRate uint64) (uint64, error) { 1049 return 0, nil 1050 } 1051 1052 func (w *TXCWallet) StandardSendFee(uint64) uint64 { return 1 } 1053 1054 func (w *TXCWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) { 1055 if w.accelerateOrderErr != nil { 1056 return 0, w.accelerateOrderErr 1057 } 1058 1059 w.accelerationParams = &struct { 1060 swapCoins []dex.Bytes 1061 accelerationCoins []dex.Bytes 1062 changeCoin dex.Bytes 1063 feeSuggestion uint64 1064 newFeeRate uint64 1065 requiredForRemainingSwaps uint64 1066 }{ 1067 swapCoins: swapCoins, 1068 accelerationCoins: accelerationCoins, 1069 changeCoin: changeCoin, 1070 requiredForRemainingSwaps: requiredForRemainingSwaps, 1071 newFeeRate: newFeeRate, 1072 } 1073 1074 return w.accelerationEstimate, nil 1075 } 1076 1077 func (w *TXCWallet) ReturnRedemptionAddress(addr string) { 1078 w.returnedAddr = addr 1079 } 1080 func (w *TXCWallet) ReturnRefundContracts(contracts [][]byte) { 1081 w.returnedContracts = contracts 1082 } 1083 func (w *TXCWallet) MaxFundingFees(_ uint32, _ uint64, _ map[string]string) uint64 { 1084 return 0 1085 } 1086 1087 func (*TXCWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) (coins []asset.Coins, redeemScripts [][]dex.Bytes, fundingFees uint64, err error) { 1088 return nil, nil, 0, nil 1089 } 1090 1091 var _ asset.Bonder = (*TXCWallet)(nil) 1092 1093 func (*TXCWallet) BondsFeeBuffer(feeRate uint64) uint64 { 1094 return 4 * 1000 * feeRate * 2 1095 } 1096 1097 func (w *TXCWallet) SetBondReserves(reserves uint64) { 1098 w.reserves.Store(reserves) 1099 } 1100 1101 func (w *TXCWallet) RefundBond(ctx context.Context, ver uint16, coinID, script []byte, amt uint64, privKey *secp256k1.PrivateKey) (asset.Coin, error) { 1102 return w.refundBondCoin, w.refundBondErr 1103 } 1104 1105 func (w *TXCWallet) FindBond(ctx context.Context, coinID []byte, searchUntil time.Time) (bond *asset.BondDetails, err error) { 1106 return w.findBond, w.findBondErr 1107 } 1108 1109 func (w *TXCWallet) MakeBondTx(ver uint16, amt, feeRate uint64, lockTime time.Time, privKey *secp256k1.PrivateKey, acctID []byte) (*asset.Bond, func(), error) { 1110 if w.makeBondTxErr != nil { 1111 return nil, nil, w.makeBondTxErr 1112 } 1113 return &asset.Bond{ 1114 Version: ver, 1115 AssetID: dcrBondAsset.ID, 1116 Amount: amt, 1117 CoinID: w.bondTxCoinID, 1118 }, func() {}, nil 1119 } 1120 1121 func (w *TXCWallet) WalletTransaction(context.Context, dex.Bytes) (*asset.WalletTransaction, error) { 1122 return nil, nil 1123 } 1124 1125 type TAccountLocker struct { 1126 *TXCWallet 1127 reserveNRedemptions uint64 1128 reserveNRedemptionsErr error 1129 reReserveRedemptionErr error 1130 redemptionUnlocked uint64 1131 reservedRedemption uint64 1132 1133 reserveNRefunds uint64 1134 reserveNRefundsErr error 1135 reReserveRefundErr error 1136 refundUnlocked uint64 1137 reservedRefund uint64 1138 } 1139 1140 var _ asset.AccountLocker = (*TAccountLocker)(nil) 1141 1142 func newTAccountLocker(assetID uint32) (*xcWallet, *TAccountLocker) { 1143 xcWallet, tWallet := newTWallet(assetID) 1144 accountLocker := &TAccountLocker{TXCWallet: tWallet} 1145 xcWallet.Wallet = accountLocker 1146 return xcWallet, accountLocker 1147 } 1148 1149 func (w *TAccountLocker) ReserveNRedemptions(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) { 1150 return w.reserveNRedemptions, w.reserveNRedemptionsErr 1151 } 1152 1153 func (w *TAccountLocker) ReReserveRedemption(v uint64) error { 1154 w.fundingMtx.Lock() 1155 defer w.fundingMtx.Unlock() 1156 w.reservedRedemption += v 1157 return w.reReserveRedemptionErr 1158 } 1159 1160 func (w *TAccountLocker) UnlockRedemptionReserves(v uint64) { 1161 w.fundingMtx.Lock() 1162 defer w.fundingMtx.Unlock() 1163 w.redemptionUnlocked += v 1164 } 1165 1166 func (w *TAccountLocker) ReserveNRefunds(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) { 1167 return w.reserveNRefunds, w.reserveNRefundsErr 1168 } 1169 1170 func (w *TAccountLocker) UnlockRefundReserves(v uint64) { 1171 w.fundingMtx.Lock() 1172 defer w.fundingMtx.Unlock() 1173 w.refundUnlocked += v 1174 } 1175 1176 func (w *TAccountLocker) ReReserveRefund(v uint64) error { 1177 w.fundingMtx.Lock() 1178 defer w.fundingMtx.Unlock() 1179 w.reservedRefund += v 1180 return w.reReserveRefundErr 1181 } 1182 1183 type TFeeRater struct { 1184 *TXCWallet 1185 feeRate uint64 1186 } 1187 1188 func (w *TFeeRater) FeeRate() uint64 { 1189 return w.feeRate 1190 } 1191 1192 type TLiveReconfigurer struct { 1193 *TXCWallet 1194 restart bool 1195 reconfigErr error 1196 } 1197 1198 func (r *TLiveReconfigurer) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, currentAddress string) (restartRequired bool, err error) { 1199 return r.restart, r.reconfigErr 1200 } 1201 1202 type tCrypterSmart struct { 1203 params []byte 1204 encryptErr error 1205 decryptErr error 1206 recryptErr error 1207 } 1208 1209 func newTCrypterSmart() *tCrypterSmart { 1210 return &tCrypterSmart{ 1211 params: encode.RandomBytes(5), 1212 } 1213 } 1214 1215 // Encrypt appends 8 random bytes to given []byte to mock. 1216 func (c *tCrypterSmart) Encrypt(b []byte) ([]byte, error) { 1217 randSuffix := make([]byte, 8) 1218 crand.Read(randSuffix) 1219 b = append(b, randSuffix...) 1220 return b, c.encryptErr 1221 } 1222 1223 // Decrypt deletes the last 8 bytes from given []byte. 1224 func (c *tCrypterSmart) Decrypt(b []byte) ([]byte, error) { 1225 return b[:len(b)-8], c.decryptErr 1226 } 1227 1228 func (c *tCrypterSmart) Serialize() []byte { return c.params } 1229 1230 func (c *tCrypterSmart) Close() {} 1231 1232 type tCrypter struct { 1233 encryptErr error 1234 decryptErr error 1235 recryptErr error 1236 } 1237 1238 func (c *tCrypter) Encrypt(b []byte) ([]byte, error) { 1239 return b, c.encryptErr 1240 } 1241 1242 func (c *tCrypter) Decrypt(b []byte) ([]byte, error) { 1243 return b, c.decryptErr 1244 } 1245 1246 func (c *tCrypter) Serialize() []byte { return nil } 1247 1248 func (c *tCrypter) Close() {} 1249 1250 var tAssetID uint32 1251 1252 func randomAsset() *msgjson.Asset { 1253 tAssetID++ 1254 return &msgjson.Asset{ 1255 Symbol: "BT" + strconv.Itoa(int(tAssetID)), 1256 ID: tAssetID, 1257 Version: tAssetID * 2, 1258 } 1259 } 1260 1261 func randomMsgMarket() (baseAsset, quoteAsset *msgjson.Asset) { 1262 return randomAsset(), randomAsset() 1263 } 1264 1265 func tFetcher(_ context.Context, log dex.Logger, _ map[uint32]*SupportedAsset) map[uint32]float64 { 1266 return map[uint32]float64{ 1267 tUTXOAssetA.ID: 45, 1268 tUTXOAssetB.ID: 32000, 1269 } 1270 } 1271 1272 type testRig struct { 1273 shutdown func() 1274 core *Core 1275 db *TDB 1276 queue *wait.TickerQueue 1277 ws *TWebsocket 1278 dc *dexConnection 1279 acct *dexAccount 1280 crypter encrypt.Crypter 1281 } 1282 1283 func newTestRig() *testRig { 1284 tdb := &TDB{ 1285 orderOrders: make(map[order.OrderID]*db.MetaOrder), 1286 wallet: &db.Wallet{}, 1287 existValues: map[string]bool{}, 1288 legacyKeyErr: tErr, 1289 } 1290 1291 // Set the global waiter expiration, and start the waiter. 1292 queue := wait.NewTickerQueue(time.Millisecond * 5) 1293 ctx, cancel := context.WithCancel(tCtx) 1294 var wg sync.WaitGroup 1295 wg.Add(1) 1296 go func() { 1297 defer wg.Done() 1298 queue.Run(ctx) 1299 }() 1300 1301 crypter := &tCrypter{} 1302 dc, conn, acct := testDexConnection(ctx, crypter) // crypter makes acct.encKey consistent with privKey 1303 1304 ai := &db.AccountInfo{ 1305 Host: "somedex.com", 1306 Cert: acct.cert, 1307 DEXPubKey: acct.dexPubKey, 1308 EncKeyV2: acct.encKey, 1309 } 1310 tdb.acct = ai 1311 1312 shutdown := func() { 1313 cancel() 1314 wg.Wait() 1315 dc.connMaster.Wait() 1316 } 1317 1318 rig := &testRig{ 1319 shutdown: shutdown, 1320 core: &Core{ 1321 ctx: ctx, 1322 cfg: &Config{}, 1323 db: tdb, 1324 log: tLogger, 1325 latencyQ: queue, 1326 conns: map[string]*dexConnection{ 1327 tDexHost: dc, 1328 }, 1329 lockTimeTaker: dex.LockTimeTaker(dex.Testnet), 1330 lockTimeMaker: dex.LockTimeMaker(dex.Testnet), 1331 wallets: make(map[uint32]*xcWallet), 1332 blockWaiters: make(map[string]*blockWaiter), 1333 sentCommits: make(map[order.Commitment]chan struct{}), 1334 tickSched: make(map[order.OrderID]*time.Timer), 1335 wsConstructor: func(*comms.WsCfg) (comms.WsConn, error) { 1336 // This is not very realistic since it doesn't start a fresh 1337 // one, and (*Core).connectDEX always gets the same TWebsocket, 1338 // which may have been previously "disconnected". 1339 return conn, nil 1340 }, 1341 newCrypter: func([]byte) encrypt.Crypter { return crypter }, 1342 reCrypter: func([]byte, []byte) (encrypt.Crypter, error) { return crypter, crypter.recryptErr }, 1343 noteChans: make(map[uint64]chan Notification), 1344 1345 fiatRateSources: make(map[string]*commonRateSource), 1346 notes: make(chan asset.WalletNotification, 128), 1347 pokesCache: newPokesCache(pokesCapacity), 1348 requestedActions: make(map[string]*asset.ActionRequiredNote), 1349 }, 1350 db: tdb, 1351 queue: queue, 1352 ws: conn, 1353 dc: dc, 1354 acct: acct, 1355 crypter: crypter, 1356 } 1357 1358 rig.core.intl.Store(&locale{ 1359 m: originLocale, 1360 printer: message.NewPrinter(language.AmericanEnglish), 1361 }) 1362 1363 rig.core.InitializeClient(tPW, nil) 1364 1365 // tCrypter doesn't actually use random bytes supplied by InitializeClient, 1366 // (the crypter is known ahead of time) but if that changes, we would need 1367 // to encrypt the acct.privKey here, after InitializeClient generates a new 1368 // random inner key/crypter: rig.resetAcctEncKey(tPW) 1369 1370 return rig 1371 } 1372 1373 // Encrypt acct.privKey -> acct.encKey if InitializeClient generates a new 1374 // random inner key/crypter that is different from the one used on construction. 1375 // Important if Core's crypters actually use their initialization data (random 1376 // bytes for inner crypter and the pw for outer). 1377 func (rig *testRig) resetAcctEncKey(pw []byte) error { 1378 innerCrypter, err := rig.core.encryptionKey(pw) 1379 if err != nil { 1380 return fmt.Errorf("encryptionKey error: %w", err) 1381 } 1382 encKey, err := innerCrypter.Encrypt(rig.acct.privKey.Serialize()) 1383 if err != nil { 1384 return fmt.Errorf("crypter.Encrypt error: %w", err) 1385 } 1386 rig.acct.encKey = encKey 1387 return nil 1388 } 1389 1390 func (rig *testRig) queueConfig() { 1391 rig.ws.queueResponse(msgjson.ConfigRoute, func(msg *msgjson.Message, f msgFunc) error { 1392 resp, _ := msgjson.NewResponse(msg.ID, rig.dc.cfg, nil) 1393 f(resp) 1394 return nil 1395 }) 1396 } 1397 1398 func (rig *testRig) queuePrevalidateBond() { 1399 rig.ws.queueResponse(msgjson.PreValidateBondRoute, func(msg *msgjson.Message, f msgFunc) error { 1400 preEval := new(msgjson.PreValidateBond) 1401 msg.Unmarshal(preEval) 1402 1403 preEvalResult := &msgjson.PreValidateBondResult{ 1404 AccountID: rig.dc.acct.id[:], 1405 AssetID: preEval.AssetID, 1406 Amount: dcrBondAsset.Amt, 1407 // Expiry: , 1408 } 1409 sign(tDexPriv, preEvalResult) 1410 resp, _ := msgjson.NewResponse(msg.ID, preEvalResult, nil) 1411 f(resp) 1412 return nil 1413 }) 1414 } 1415 1416 func (rig *testRig) queuePostBond(postBondResult *msgjson.PostBondResult) { 1417 rig.ws.queueResponse(msgjson.PostBondRoute, func(msg *msgjson.Message, f msgFunc) error { 1418 bond := new(msgjson.PostBond) 1419 msg.Unmarshal(bond) 1420 rig.ws.submittedBond = bond 1421 postBondResult.BondID = bond.CoinID 1422 sign(tDexPriv, postBondResult) 1423 resp, _ := msgjson.NewResponse(msg.ID, postBondResult, nil) 1424 f(resp) 1425 return nil 1426 }) 1427 } 1428 1429 func (rig *testRig) queueConnect(rpcErr *msgjson.Error, matches []*msgjson.Match, orders []*msgjson.OrderStatus, suspended ...bool) { 1430 rig.ws.queueResponse(msgjson.ConnectRoute, func(msg *msgjson.Message, f msgFunc) error { 1431 if rpcErr != nil { 1432 resp, _ := msgjson.NewResponse(msg.ID, nil, rpcErr) 1433 f(resp) 1434 return nil 1435 } 1436 1437 connect := new(msgjson.Connect) 1438 msg.Unmarshal(connect) 1439 sign(tDexPriv, connect) 1440 1441 activeBonds := make([]*msgjson.Bond, 0, 1) 1442 if b := rig.ws.submittedBond; b != nil { 1443 activeBonds = append(activeBonds, &msgjson.Bond{ 1444 Version: b.Version, 1445 Amount: dcrBondAsset.Amt, 1446 Expiry: rig.ws.liveBondExpiry, 1447 CoinID: b.CoinID, 1448 AssetID: b.AssetID, 1449 }) 1450 } 1451 1452 result := &msgjson.ConnectResult{ 1453 Sig: connect.Sig, 1454 ActiveMatches: matches, 1455 ActiveOrderStatuses: orders, 1456 ActiveBonds: activeBonds, 1457 Score: 10, 1458 Reputation: &account.Reputation{BondedTier: 1}, 1459 } 1460 if len(suspended) > 0 && suspended[0] { 1461 result.Reputation.Penalties = 1 1462 } 1463 resp, _ := msgjson.NewResponse(msg.ID, result, nil) 1464 f(resp) 1465 return nil 1466 }) 1467 } 1468 1469 func (rig *testRig) queueCancel(rpcErr *msgjson.Error) { 1470 rig.ws.queueResponse(msgjson.CancelRoute, func(msg *msgjson.Message, f msgFunc) error { 1471 var resp *msgjson.Message 1472 if rpcErr == nil { 1473 // Need to stamp and sign the message with the server's key. 1474 msgOrder := new(msgjson.CancelOrder) 1475 err := msg.Unmarshal(msgOrder) 1476 if err != nil { 1477 rpcErr = msgjson.NewError(msgjson.RPCParseError, "unable to unmarshal request") 1478 } else { 1479 co := convertMsgCancelOrder(msgOrder) 1480 resp = orderResponse(msg.ID, msgOrder, co, false, false, false) 1481 } 1482 } 1483 if rpcErr != nil { 1484 resp, _ = msgjson.NewResponse(msg.ID, nil, rpcErr) 1485 } 1486 f(resp) 1487 return nil 1488 }) 1489 } 1490 1491 func TestMain(m *testing.M) { 1492 var shutdown context.CancelFunc 1493 tCtx, shutdown = context.WithCancel(context.Background()) 1494 tDexPriv, _ = secp256k1.GeneratePrivateKey() 1495 tDexKey = tDexPriv.PubKey() 1496 1497 doIt := func() int { 1498 // Not counted as coverage, must test Archiver constructor explicitly. 1499 defer shutdown() 1500 return m.Run() 1501 } 1502 os.Exit(doIt()) 1503 } 1504 1505 func TestMarkets(t *testing.T) { 1506 rig := newTestRig() 1507 defer rig.shutdown() 1508 // The test rig's dexConnection comes with a market. Clear that for this test. 1509 rig.dc.cfgMtx.Lock() 1510 rig.dc.cfg.Markets = nil 1511 rig.dc.cfgMtx.Unlock() 1512 numMarkets := 10 1513 1514 tCore := rig.core 1515 // Simulate 10 markets. 1516 marketIDs := make(map[string]struct{}) 1517 for i := 0; i < numMarkets; i++ { 1518 base, quote := randomMsgMarket() 1519 marketIDs[marketName(base.ID, quote.ID)] = struct{}{} 1520 rig.dc.cfgMtx.RLock() 1521 cfg := rig.dc.cfg 1522 rig.dc.cfgMtx.RUnlock() 1523 cfg.Markets = append(cfg.Markets, &msgjson.Market{ 1524 Name: base.Symbol + quote.Symbol, 1525 Base: base.ID, 1526 Quote: quote.ID, 1527 EpochLen: 5000, 1528 MarketBuyBuffer: 1.4, 1529 }) 1530 rig.dc.assetsMtx.Lock() 1531 rig.dc.assets[base.ID] = convertAssetInfo(base) 1532 rig.dc.assets[quote.ID] = convertAssetInfo(quote) 1533 rig.dc.assetsMtx.Unlock() 1534 } 1535 1536 // Just check that the information is coming through correctly. 1537 xcs := tCore.Exchanges() 1538 if len(xcs) != 1 { 1539 t.Fatalf("expected 1 MarketInfo, got %d", len(xcs)) 1540 } 1541 1542 rig.dc.assetsMtx.RLock() 1543 defer rig.dc.assetsMtx.RUnlock() 1544 1545 assets := rig.dc.assets 1546 for _, xc := range xcs { 1547 for _, market := range xc.Markets { 1548 mkt := marketName(market.BaseID, market.QuoteID) 1549 _, found := marketIDs[mkt] 1550 if !found { 1551 t.Fatalf("market %s not found", mkt) 1552 } 1553 if assets[market.BaseID].Symbol != market.BaseSymbol { 1554 t.Fatalf("base symbol mismatch. %s != %s", assets[market.BaseID].Symbol, market.BaseSymbol) 1555 } 1556 if assets[market.QuoteID].Symbol != market.QuoteSymbol { 1557 t.Fatalf("quote symbol mismatch. %s != %s", assets[market.QuoteID].Symbol, market.QuoteSymbol) 1558 } 1559 } 1560 } 1561 } 1562 1563 func TestBookFeed(t *testing.T) { 1564 rig := newTestRig() 1565 defer rig.shutdown() 1566 tCore := rig.core 1567 dc := rig.dc 1568 1569 checkAction := func(feed BookFeed, action string) { 1570 t.Helper() 1571 select { 1572 case u := <-feed.Next(): 1573 if u.Action != action { 1574 t.Fatalf("expected action = %s, got %s", action, u.Action) 1575 } 1576 default: 1577 t.Fatalf("no %s received", action) 1578 } 1579 } 1580 1581 // Ensure handleOrderBookMsg creates an order book as expected. 1582 oid1 := ordertest.RandomOrderID() 1583 bookMsg, err := msgjson.NewResponse(1, &msgjson.OrderBook{ 1584 Seq: 1, 1585 MarketID: tDcrBtcMktName, 1586 Orders: []*msgjson.BookOrderNote{ 1587 { 1588 TradeNote: msgjson.TradeNote{ 1589 Side: msgjson.BuyOrderNum, 1590 Quantity: 10, 1591 Rate: 2, 1592 }, 1593 OrderNote: msgjson.OrderNote{ 1594 Seq: 1, 1595 MarketID: tDcrBtcMktName, 1596 OrderID: oid1[:], 1597 }, 1598 }, 1599 }, 1600 }, nil) 1601 if err != nil { 1602 t.Fatalf("[NewResponse]: unexpected err: %v", err) 1603 } 1604 1605 oid2 := ordertest.RandomOrderID() 1606 bookNote, _ := msgjson.NewNotification(msgjson.BookOrderRoute, &msgjson.BookOrderNote{ 1607 TradeNote: msgjson.TradeNote{ 1608 Side: msgjson.BuyOrderNum, 1609 Quantity: 10, 1610 Rate: 2, 1611 }, 1612 OrderNote: msgjson.OrderNote{ 1613 Seq: 2, 1614 MarketID: tDcrBtcMktName, 1615 OrderID: oid2[:], 1616 }, 1617 }) 1618 1619 err = handleBookOrderMsg(tCore, dc, bookNote) 1620 if err == nil { 1621 t.Fatalf("no error for missing book") 1622 } 1623 1624 // Sync to unknown dex 1625 _, _, err = tCore.SyncBook("unknown dex", tUTXOAssetA.ID, tUTXOAssetB.ID) 1626 if err == nil { 1627 t.Fatalf("no error for unknown dex") 1628 } 1629 _, _, err = tCore.SyncBook(tDexHost, tUTXOAssetA.ID, 12345) 1630 if err == nil { 1631 t.Fatalf("no error for nonsense market") 1632 } 1633 1634 // Success 1635 rig.ws.queueResponse(msgjson.OrderBookRoute, func(msg *msgjson.Message, f msgFunc) error { 1636 f(bookMsg) 1637 return nil 1638 }) 1639 _, feed1, err := tCore.SyncBook(tDexHost, tUTXOAssetA.ID, tUTXOAssetB.ID) 1640 if err != nil { 1641 t.Fatalf("SyncBook 1 error: %v", err) 1642 } 1643 _, feed2, err := tCore.SyncBook(tDexHost, tUTXOAssetA.ID, tUTXOAssetB.ID) 1644 if err != nil { 1645 t.Fatalf("SyncBook 2 error: %v", err) 1646 } 1647 1648 // Should be able to retrieve the book now. 1649 book, err := tCore.Book(tDexHost, tUTXOAssetA.ID, tUTXOAssetB.ID) 1650 if err != nil { 1651 t.Fatalf("Core.Book error: %v", err) 1652 } 1653 // Should have one buy order 1654 if len(book.Buys) != 1 { 1655 t.Fatalf("no buy orders found. expected 1") 1656 } 1657 1658 // Both channels should have a full orderbook. 1659 checkAction(feed1, FreshBookAction) 1660 checkAction(feed2, FreshBookAction) 1661 1662 err = handleBookOrderMsg(tCore, dc, bookNote) 1663 if err != nil { 1664 t.Fatalf("[handleBookOrderMsg]: unexpected err: %v", err) 1665 } 1666 1667 // Both channels should have an update. 1668 checkAction(feed1, BookOrderAction) 1669 checkAction(feed2, BookOrderAction) 1670 1671 // Close feed 1 1672 feed1.Close() 1673 1674 oid3 := ordertest.RandomOrderID() 1675 bookNote, _ = msgjson.NewNotification(msgjson.BookOrderRoute, &msgjson.BookOrderNote{ 1676 TradeNote: msgjson.TradeNote{ 1677 Side: msgjson.SellOrderNum, 1678 Quantity: 10, 1679 Rate: 3, 1680 }, 1681 OrderNote: msgjson.OrderNote{ 1682 Seq: 3, 1683 MarketID: tDcrBtcMktName, 1684 OrderID: oid3[:], 1685 }, 1686 }) 1687 err = handleBookOrderMsg(tCore, dc, bookNote) 1688 if err != nil { 1689 t.Fatalf("[handleBookOrderMsg]: unexpected err: %v", err) 1690 } 1691 1692 // feed1 should have no update 1693 select { 1694 case <-feed1.Next(): 1695 t.Fatalf("update for feed 1 after Close") 1696 default: 1697 } 1698 // feed2 should though 1699 checkAction(feed2, BookOrderAction) 1700 1701 // Make sure the book has been updated. 1702 book, _ = tCore.Book(tDexHost, tUTXOAssetA.ID, tUTXOAssetB.ID) 1703 if len(book.Buys) != 2 { 1704 t.Fatalf("expected 2 buys, got %d", len(book.Buys)) 1705 } 1706 if len(book.Sells) != 1 { 1707 t.Fatalf("expected 1 sell, got %d", len(book.Sells)) 1708 } 1709 1710 // Update the remaining quantity of the just booked order. 1711 var remaining uint64 = 5 * 1e8 1712 bookNote, _ = msgjson.NewNotification(msgjson.BookOrderRoute, &msgjson.UpdateRemainingNote{ 1713 OrderNote: msgjson.OrderNote{ 1714 Seq: 4, 1715 MarketID: tDcrBtcMktName, 1716 OrderID: oid3[:], 1717 }, 1718 Remaining: remaining, 1719 }) 1720 err = handleUpdateRemainingMsg(tCore, dc, bookNote) 1721 if err != nil { 1722 t.Fatalf("[handleBookOrderMsg]: unexpected err: %v", err) 1723 } 1724 1725 // feed2 should have an update 1726 checkAction(feed2, UpdateRemainingAction) 1727 book, _ = tCore.Book(tDexHost, tUTXOAssetA.ID, tUTXOAssetB.ID) 1728 firstSellQty := book.Sells[0].QtyAtomic 1729 if firstSellQty != remaining { 1730 t.Fatalf("expected remaining quantity of %d after update_remaining. got %d", remaining, firstSellQty) 1731 } 1732 1733 // Ensure handleUnbookOrderMsg removes a book order from an associated 1734 // order book as expected. 1735 unbookNote, _ := msgjson.NewNotification(msgjson.UnbookOrderRoute, &msgjson.UnbookOrderNote{ 1736 Seq: 5, 1737 MarketID: tDcrBtcMktName, 1738 OrderID: oid1[:], 1739 }) 1740 1741 err = handleUnbookOrderMsg(tCore, dc, unbookNote) 1742 if err != nil { 1743 t.Fatalf("[handleUnbookOrderMsg]: unexpected err: %v", err) 1744 } 1745 // feed2 should have a notification. 1746 checkAction(feed2, UnbookOrderAction) 1747 book, _ = tCore.Book(tDexHost, tUTXOAssetA.ID, tUTXOAssetB.ID) 1748 if len(book.Buys) != 1 { 1749 t.Fatalf("expected 1 buy after unbook_order, got %d", len(book.Buys)) 1750 } 1751 1752 // Test candles 1753 queueCandles := func() { 1754 rig.ws.queueResponse(msgjson.CandlesRoute, func(msg *msgjson.Message, f msgFunc) error { 1755 resp, _ := msgjson.NewResponse(msg.ID, &msgjson.WireCandles{ 1756 StartStamps: []uint64{1, 2}, 1757 EndStamps: []uint64{3, 4}, 1758 MatchVolumes: []uint64{1, 2}, 1759 QuoteVolumes: []uint64{1, 2}, 1760 HighRates: []uint64{3, 4}, 1761 LowRates: []uint64{1, 2}, 1762 StartRates: []uint64{1, 2}, 1763 EndRates: []uint64{3, 4}, 1764 }, nil) 1765 f(resp) 1766 return nil 1767 }) 1768 } 1769 queueCandles() 1770 1771 if err := feed2.Candles("1h"); err != nil { 1772 t.Fatalf("Candles error: %v", err) 1773 } 1774 1775 checkAction(feed2, FreshCandlesAction) 1776 1777 // An epoch report should trigger two candle updates, one for each bin size. 1778 epochReport, _ := msgjson.NewNotification(msgjson.EpochReportRoute, &msgjson.EpochReportNote{ 1779 MarketID: tDcrBtcMktName, 1780 Epoch: 1, 1781 BaseFeeRate: 2, 1782 QuoteFeeRate: 3, 1783 Candle: msgjson.Candle{ 1784 StartStamp: 1, 1785 EndStamp: 2, 1786 MatchVolume: 3, 1787 QuoteVolume: 3, 1788 HighRate: 4, 1789 LowRate: 1, 1790 StartRate: 1, 1791 EndRate: 2, 1792 }, 1793 }) 1794 1795 if err := handleEpochReportMsg(tCore, dc, epochReport); err != nil { 1796 t.Fatalf("handleEpochReportMsg error: %v", err) 1797 } 1798 1799 checkAction(feed2, EpochMatchSummary) 1800 1801 // We'll only receive 1 candle update, since we only synced one set of 1802 // candles so far. 1803 checkAction(feed2, CandleUpdateAction) 1804 checkAction(feed2, EpochResolved) 1805 1806 // Now subscribe to the 24h candles too. 1807 queueCandles() 1808 if err := feed2.Candles("24h"); err != nil { 1809 t.Fatalf("24h Candles error: %v", err) 1810 } 1811 checkAction(feed2, FreshCandlesAction) 1812 1813 // This time, an epoch report should trigger two updates. 1814 if err := handleEpochReportMsg(tCore, dc, epochReport); err != nil { 1815 t.Fatalf("handleEpochReportMsg error: %v", err) 1816 } 1817 checkAction(feed2, EpochMatchSummary) 1818 checkAction(feed2, CandleUpdateAction) 1819 checkAction(feed2, CandleUpdateAction) 1820 } 1821 1822 type tDriver struct { 1823 wallet asset.Wallet 1824 decodedCoinID string 1825 winfo *asset.WalletInfo 1826 } 1827 1828 func (drv *tDriver) Open(cfg *asset.WalletConfig, logger dex.Logger, net dex.Network) (asset.Wallet, error) { 1829 return drv.wallet, nil 1830 } 1831 1832 func (drv *tDriver) DecodeCoinID(coinID []byte) (string, error) { 1833 return drv.decodedCoinID, nil 1834 } 1835 1836 func (drv *tDriver) Info() *asset.WalletInfo { 1837 return drv.winfo 1838 } 1839 1840 type tCreator struct { 1841 *tDriver 1842 doesntExist bool 1843 existsErr error 1844 createErr error 1845 } 1846 1847 func (ctr *tCreator) Exists(walletType, dataDir string, settings map[string]string, net dex.Network) (bool, error) { 1848 return !ctr.doesntExist, ctr.existsErr 1849 } 1850 1851 func (ctr *tCreator) Create(*asset.CreateWalletParams) error { 1852 return ctr.createErr 1853 } 1854 1855 func TestCreateWallet(t *testing.T) { 1856 rig := newTestRig() 1857 defer rig.shutdown() 1858 tCore := rig.core 1859 1860 // Create a new asset. 1861 a := *tUTXOAssetA 1862 tILT := &a 1863 tILT.Symbol = "ilt" 1864 tILT.ID, _ = dex.BipSymbolID(tILT.Symbol) 1865 1866 // Create registration form. 1867 form := &WalletForm{ 1868 AssetID: tILT.ID, 1869 Config: map[string]string{ 1870 "rpclisten": "localhost", 1871 }, 1872 Type: "type", 1873 } 1874 1875 ensureErr := func(tag string) { 1876 t.Helper() 1877 err := tCore.CreateWallet(tPW, wPW, form) 1878 if err == nil { 1879 t.Fatalf("no %s error", tag) 1880 } 1881 } 1882 1883 // Try to add an existing wallet. 1884 wallet, tWallet := newTWallet(tILT.ID) 1885 tCore.wallets[tILT.ID] = wallet 1886 ensureErr("existing wallet") 1887 delete(tCore.wallets, tILT.ID) 1888 1889 // Failure to retrieve encryption key params. 1890 creds := tCore.credentials 1891 tCore.credentials = nil 1892 ensureErr("db.Get") 1893 tCore.credentials = creds 1894 1895 // Crypter error. 1896 rig.crypter.(*tCrypter).encryptErr = tErr 1897 ensureErr("Encrypt") 1898 rig.crypter.(*tCrypter).encryptErr = nil 1899 1900 // Try an unknown wallet (not yet asset.Register'ed). 1901 ensureErr("unregistered asset") 1902 1903 // Register the asset. 1904 asset.Register(tILT.ID, &tDriver{ 1905 wallet: wallet.Wallet, 1906 decodedCoinID: "ilt-coin", 1907 winfo: tWalletInfo, 1908 }) 1909 1910 // Connection error. 1911 tWallet.connectErr = tErr 1912 ensureErr("Connect") 1913 tWallet.connectErr = nil 1914 1915 // Unlock error. 1916 tWallet.unlockErr = tErr 1917 ensureErr("Unlock") 1918 tWallet.unlockErr = nil 1919 1920 // Address error. 1921 tWallet.addrErr = tErr 1922 ensureErr("Address") 1923 tWallet.addrErr = nil 1924 1925 // Balance error. 1926 tWallet.balErr = tErr 1927 ensureErr("Balance") 1928 tWallet.balErr = nil 1929 1930 // Database error. 1931 rig.db.updateWalletErr = tErr 1932 ensureErr("db.UpdateWallet") 1933 rig.db.updateWalletErr = nil 1934 1935 // Success 1936 delete(tCore.wallets, tILT.ID) 1937 err := tCore.CreateWallet(tPW, wPW, form) 1938 if err != nil { 1939 t.Fatalf("error when should be no error: %v", err) 1940 } 1941 } 1942 1943 // TODO: TestGetDEXConfig 1944 /* 1945 func TestGetFee(t *testing.T) { 1946 rig := newTestRig() 1947 defer rig.shutdown() 1948 tCore := rig.core 1949 cert := []byte{} 1950 1951 // DEX already registered 1952 _, err := tCore.GetFee(tDexHost, cert) 1953 if !errorHasCode(err, dupeDEXErr) { 1954 t.Fatalf("wrong account exists error: %v", err) 1955 } 1956 1957 // Lose the dexConnection 1958 tCore.connMtx.Lock() 1959 delete(tCore.conns, tDexHost) 1960 tCore.connMtx.Unlock() 1961 1962 // connectDEX error 1963 _, err = tCore.GetFee(tUnparseableHost, cert) 1964 if !errorHasCode(err, addressParseErr) { 1965 t.Fatalf("wrong connectDEX error: %v", err) 1966 } 1967 1968 // Queue a config response for success 1969 rig.queueConfig() 1970 1971 // Success 1972 _, err = tCore.GetFee(tDexHost, cert) 1973 if err != nil { 1974 t.Fatalf("GetFee error: %v", err) 1975 } 1976 } 1977 */ 1978 1979 func TestPostBond(t *testing.T) { 1980 // This test takes a little longer because the key is decrypted every time 1981 // Register is called. 1982 rig := newTestRig() 1983 defer rig.shutdown() 1984 tCore := rig.core 1985 dc := rig.dc 1986 clearConn := func() { 1987 tCore.connMtx.Lock() 1988 delete(tCore.conns, tDexHost) 1989 tCore.connMtx.Unlock() 1990 } 1991 clearConn() 1992 1993 wallet, tWallet := newTWallet(tUTXOAssetA.ID) 1994 tCore.wallets[tUTXOAssetA.ID] = wallet 1995 tWallet.bal = &asset.Balance{ 1996 Available: 4e9, 1997 } 1998 1999 // When registering, successfully retrieving *db.AccountInfo from the DB is 2000 // an error (no dupes). Initial state is to return an error. 2001 rig.db.acctErr = tErr 2002 2003 _ = tCore.Login(tPW) 2004 2005 // (*Core).Register does setupCryptoV2 to make the dc.acct.privKey etc., so 2006 // we don't know the ClientPubKey here. It must be set in the request 2007 // handler configured by queueRegister. 2008 rig.ws.liveBondExpiry = uint64(time.Now().Add(time.Duration(pendingBuffer(dex.Simnet)) * 2 * time.Second).Unix()) 2009 postBondResult := &msgjson.PostBondResult{ 2010 AccountID: rig.acct.id[:], 2011 AssetID: dcrBondAsset.ID, 2012 Amount: dcrBondAsset.Amt, 2013 Expiry: rig.ws.liveBondExpiry, 2014 Reputation: &account.Reputation{BondedTier: 1}, 2015 } 2016 2017 var wg sync.WaitGroup 2018 defer wg.Wait() // don't allow fail after TestRegister return 2019 2020 queueTipChange := func() { 2021 wg.Add(1) 2022 go func() { 2023 defer wg.Done() 2024 timeout := time.NewTimer(time.Second * 2) 2025 defer timeout.Stop() 2026 ticker := time.NewTicker(10 * time.Millisecond) 2027 defer ticker.Stop() 2028 for { 2029 select { 2030 case <-ticker.C: 2031 tCore.waiterMtx.Lock() 2032 waiterCount := len(tCore.blockWaiters) 2033 tCore.waiterMtx.Unlock() 2034 2035 // Every tick, increase the bond tx confirmation count. 2036 if waiterCount > 0 { // when verifyRegistrationFee adds a waiter, then we can trigger tip change 2037 confs, found := tWallet.confs[dex.Bytes(tWallet.bondTxCoinID).String()] 2038 if !found { 2039 tWallet.setConfs(tWallet.bondTxCoinID, 0, nil) 2040 } else { 2041 tWallet.setConfs(tWallet.bondTxCoinID, confs+1, nil) 2042 } 2043 2044 tCore.tipChange(tUTXOAssetA.ID) 2045 return 2046 } 2047 case <-timeout.C: 2048 t.Errorf("failed to find waiter before timeout") 2049 return 2050 } 2051 } 2052 }() 2053 } 2054 2055 accountNotFoundError := msgjson.NewError(msgjson.AccountNotFoundError, "test account not found error") 2056 2057 queueConfigAndConnectUnknownAcct := func() { 2058 rig.ws.submittedBond = nil 2059 rig.queueConfig() 2060 rig.queueConnect(accountNotFoundError, nil, nil) // for discoverAccount 2061 rig.ws.queueResponse(msgjson.FeeRateRoute, func(msg *msgjson.Message, f msgFunc) error { 2062 const feeRate = 50 2063 resp, _ := msgjson.NewResponse(msg.ID, feeRate, nil) 2064 f(resp) 2065 return nil 2066 }) 2067 } 2068 2069 queuePostBondSequence := func() { 2070 rig.queuePrevalidateBond() 2071 rig.queuePostBond(postBondResult) 2072 queueTipChange() 2073 rig.queueConnect(nil, nil, nil) 2074 } 2075 2076 queueResponses := func() { 2077 queueConfigAndConnectUnknownAcct() 2078 queuePostBondSequence() 2079 } 2080 2081 form := &PostBondForm{ 2082 Addr: tDexHost, 2083 AppPass: tPW, 2084 Asset: &dcrBondAsset.ID, 2085 Bond: dcrBondAsset.Amt, 2086 Cert: []byte{0x1}, // not empty signals TLS, otherwise no TLS allowed hidden services 2087 } 2088 2089 // Suppress warnings about SendTransaction returning a mismatching ID. 2090 tWallet.feeCoinSent = tWallet.bondTxCoinID 2091 2092 ch := tCore.NotificationFeed() 2093 2094 var err error 2095 run := func() { 2096 // Register method will error if url is already in conns map. 2097 clearConn() 2098 2099 tWallet.setConfs(tWallet.bondTxCoinID, 0, nil) 2100 // Skip finding bonds. 2101 tWallet.findBondErr = errors.New("purposeful error") 2102 _, err = tCore.PostBond(form) 2103 } 2104 2105 getNotification := func(tag string) any { 2106 t.Helper() 2107 select { 2108 case n := <-ch.C: 2109 return n 2110 // When it works, it should be virtually instant, but I have seen it fail 2111 // at 1 millisecond. 2112 case <-time.NewTimer(time.Second * 2).C: 2113 t.Fatalf("timed out waiting for %s notification", tag) 2114 } 2115 return nil 2116 } 2117 2118 // The feepayment note for mined fee payment txn notification to server, and 2119 // the balance note from tip change are concurrent and thus come in no 2120 // guaranteed order. 2121 getBondAndBalanceNote := func() { 2122 t.Helper() 2123 var bondNote *BondPostNote 2124 var balanceNotes uint8 2125 // For a normal PostBond, there are three balance updates. 2126 // 1) makeAndPostBond, 2) monitorBondConfs.trigger, and 3) tipChange. 2127 for bondNote == nil || balanceNotes < 3 { 2128 ntfn := getNotification("bond posted or balance") 2129 switch note := ntfn.(type) { 2130 case *BondPostNote: 2131 if note.TopicID == TopicAccountRegistered { 2132 bondNote = note 2133 } 2134 case *BalanceNote: 2135 balanceNotes++ 2136 case *ReputationNote: // ignore 2137 default: 2138 t.Fatalf("wrong notification (%T). Expected FeePaymentNote or BalanceNote", ntfn) 2139 } 2140 } 2141 } 2142 2143 queueResponses() 2144 run() 2145 if err != nil { 2146 t.Fatalf("postbond error: %v", err) 2147 } 2148 2149 // Should be two success notifications. One for fee paid on-chain, one for 2150 // fee notification sent, each along with a balance note. 2151 getBondAndBalanceNote() 2152 2153 // password error 2154 rig.crypter.(*tCrypter).recryptErr = tErr 2155 run() 2156 if !errorHasCode(err, passwordErr) { 2157 t.Fatalf("wrong password error: %v", err) 2158 } 2159 rig.crypter.(*tCrypter).recryptErr = nil 2160 2161 // no host error 2162 form.Addr = "" 2163 run() 2164 if !errorHasCode(err, emptyHostErr) { 2165 t.Fatalf("wrong empty host error: %v", err) 2166 } 2167 form.Addr = tDexHost 2168 2169 // wallet not found 2170 delete(tCore.wallets, tUTXOAssetA.ID) 2171 run() 2172 if !errorHasCode(err, missingWalletErr) { 2173 t.Fatalf("wrong missing wallet error: %v", err) 2174 } 2175 tCore.wallets[tUTXOAssetA.ID] = wallet 2176 2177 // Unlock wallet error 2178 tWallet.unlockErr = tErr 2179 tWallet.locked = true 2180 run() 2181 if !errorHasCode(err, walletAuthErr) { 2182 t.Fatalf("wrong wallet auth error: %v", err) 2183 } 2184 tWallet.unlockErr = nil 2185 tWallet.locked = false 2186 2187 // connectDEX error 2188 form.Addr = tUnparseableHost 2189 run() 2190 if !errorHasCode(err, connectionErr) { 2191 t.Fatalf("wrong connectDEX error: %v", err) 2192 } 2193 form.Addr = tDexHost 2194 2195 // fee asset not found, no cfg.Fee fallback 2196 bondAssets := dc.cfg.BondAssets 2197 dc.cfg.BondAssets = nil 2198 queueConfigAndConnectUnknownAcct() 2199 run() 2200 if !errorHasCode(err, assetSupportErr) { 2201 t.Fatalf("wrong error for missing asset: %v", err) 2202 } 2203 dc.cfg.BondAssets = bondAssets 2204 2205 // error creating signing key 2206 rig.crypter.(*tCrypter).encryptErr = tErr 2207 rig.queueConfig() 2208 run() 2209 if !errorHasCode(err, acctKeyErr) { 2210 t.Fatalf("wrong account key error: %v", err) 2211 } 2212 rig.crypter.(*tCrypter).encryptErr = nil 2213 2214 bal0 := tWallet.bal.Available 2215 tWallet.bal.Available = 0 2216 run() 2217 if !errorHasCode(err, walletBalanceErr) { 2218 t.Fatalf("expected low balance error, got: %v", err) 2219 } 2220 tWallet.bal.Available = bal0 2221 2222 // signature error 2223 queueConfigAndConnectUnknownAcct() 2224 rig.ws.queueResponse(msgjson.PreValidateBondRoute, func(msg *msgjson.Message, f msgFunc) error { 2225 preEval := new(msgjson.PreValidateBond) 2226 msg.Unmarshal(preEval) 2227 2228 preEvalResult := &msgjson.PreValidateBondResult{ 2229 Signature: msgjson.Signature{ 2230 Sig: []byte{0xb, 0xa, 0xd}, 2231 }, 2232 } 2233 resp, _ := msgjson.NewResponse(msg.ID, preEvalResult, nil) 2234 f(resp) 2235 return nil 2236 }) 2237 run() 2238 if !errorHasCode(err, signatureErr) { 2239 t.Fatalf("wrong error for bad signature on prevalidate response: %v", err) 2240 } 2241 2242 // Wrong bond size on form 2243 goodAmt := form.Bond 2244 form.Bond = goodAmt + 1 2245 queueConfigAndConnectUnknownAcct() 2246 run() 2247 if !errorHasCode(err, bondAmtErr) { 2248 t.Fatalf("wrong error for wrong fee in form: %v", err) 2249 } 2250 form.Bond = goodAmt 2251 2252 // MakeBondTx error 2253 queueConfigAndConnectUnknownAcct() 2254 tWallet.makeBondTxErr = tErr 2255 run() 2256 if !errorHasCode(err, bondPostErr) { 2257 t.Fatalf("wrong error for bondPostErr: %v", err) 2258 } 2259 tWallet.makeBondTxErr = nil 2260 2261 // Make sure it's good again. 2262 queueResponses() 2263 run() 2264 if err != nil { 2265 t.Fatalf("error after regaining valid state: %v", err) 2266 } 2267 getBondAndBalanceNote() 2268 2269 // Test the account recovery path. 2270 rig.queueConfig() 2271 rig.queueConnect(nil, nil, nil) // account exists 2272 run() 2273 if err != nil { 2274 t.Fatalf("Paid account error: %v", err) 2275 } 2276 2277 // Account suspended should derive new HD credentials. 2278 rig.queueConnect(nil, nil, nil, true) // first try exists but suspended 2279 queueResponses() 2280 run() 2281 if err != nil { 2282 t.Fatalf("Suspension recovery error: %v", err) 2283 } 2284 getBondAndBalanceNote() 2285 } 2286 2287 func TestCredentialsUpgrade(t *testing.T) { 2288 rig := newTestRig() 2289 defer rig.shutdown() 2290 tCore := rig.core 2291 rig.db.legacyKeyErr = nil 2292 2293 clearUpgrade := func() { 2294 rig.db.creds.EncInnerKey = nil 2295 tCore.credentials.EncInnerKey = nil 2296 } 2297 2298 clearUpgrade() 2299 2300 // initial success 2301 err := tCore.Login(tPW) 2302 if err != nil { 2303 t.Fatalf("initial Login error: %v", err) 2304 } 2305 2306 clearUpgrade() 2307 2308 // Recrypt error 2309 rig.db.recryptErr = tErr 2310 err = tCore.Login(tPW) 2311 if err == nil { 2312 t.Fatalf("no error for recryptErr") 2313 } 2314 rig.db.recryptErr = nil 2315 2316 // final success 2317 err = tCore.Login(tPW) 2318 if err != nil { 2319 t.Fatalf("final Login error: %v", err) 2320 } 2321 } 2322 2323 func unauth(a *dexAccount) { 2324 a.authMtx.Lock() 2325 a.isAuthed = false 2326 a.authMtx.Unlock() 2327 } 2328 2329 func TestLogin(t *testing.T) { 2330 rig := newTestRig() 2331 defer rig.shutdown() 2332 tCore := rig.core 2333 rig.acct.rep = account.Reputation{BondedTier: 1} 2334 2335 rig.queueConnect(nil, nil, nil) 2336 err := tCore.Login(tPW) 2337 if err != nil || !rig.acct.authed() { 2338 t.Fatalf("initial Login error: %v", err) 2339 } 2340 2341 // No encryption key. 2342 unauth(rig.acct) 2343 creds := tCore.credentials 2344 tCore.credentials = nil 2345 err = tCore.Login(tPW) 2346 if err == nil || rig.acct.authed() { 2347 t.Fatalf("no error for missing app key") 2348 } 2349 tCore.credentials = creds 2350 2351 // Account not Paid. No error, and account should be unlocked. 2352 rig.acct.rep = account.Reputation{BondedTier: 0} 2353 rig.queueConnect(nil, nil, nil) 2354 err = tCore.Login(tPW) 2355 if err != nil || rig.acct.authed() { 2356 t.Fatalf("error for unpaid account: %v", err) 2357 } 2358 if rig.acct.locked() { 2359 t.Fatalf("unpaid account is locked") 2360 } 2361 rig.acct.rep = account.Reputation{BondedTier: 1} 2362 2363 // 'connect' route error. 2364 rig = newTestRig() 2365 defer rig.shutdown() 2366 tCore = rig.core 2367 unauth(rig.acct) 2368 rig.ws.queueResponse(msgjson.ConnectRoute, func(msg *msgjson.Message, f msgFunc) error { 2369 resp, _ := msgjson.NewResponse(msg.ID, nil, msgjson.NewError(1, "test error")) 2370 f(resp) 2371 return nil 2372 }) 2373 err = tCore.Login(tPW) 2374 // Should be no error, but also not authed. Error is sent and logged 2375 // as a notification. 2376 if err != nil || rig.acct.authed() { 2377 t.Fatalf("account authed after 'connect' error") 2378 } 2379 2380 // Success with some matches in the response. 2381 rig = newTestRig() 2382 defer rig.shutdown() 2383 dc := rig.dc 2384 qty := 3 * dcrBtcLotSize 2385 lo, dbOrder, preImg, addr := makeLimitOrder(dc, true, qty, dcrBtcRateStep*10) 2386 lo.Force = order.StandingTiF 2387 dbOrder.MetaData.Status = order.OrderStatusBooked // leave unfunded to have it canceled on auth/'connect' 2388 oid := lo.ID() 2389 dcrWallet, _ := newTWallet(tUTXOAssetA.ID) 2390 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 2391 btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) 2392 tCore.wallets[tUTXOAssetB.ID] = btcWallet 2393 walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true) 2394 tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 2395 rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails) // nil means no funding coins 2396 matchID := ordertest.RandomMatchID() 2397 match := &matchTracker{ 2398 MetaMatch: db.MetaMatch{ 2399 UserMatch: &order.UserMatch{MatchID: matchID}, 2400 MetaData: &db.MatchMetaData{}, 2401 }, 2402 } 2403 tracker.matches[matchID] = match 2404 knownMsgMatch := &msgjson.Match{OrderID: oid[:], MatchID: matchID[:]} 2405 2406 // Known trade, but missing match 2407 missingID := ordertest.RandomMatchID() 2408 missingMatch := &matchTracker{ 2409 MetaMatch: db.MetaMatch{ 2410 UserMatch: &order.UserMatch{MatchID: missingID}, 2411 MetaData: &db.MatchMetaData{}, 2412 }, 2413 } 2414 tracker.matches[missingID] = missingMatch 2415 2416 // extra match 2417 extraID := ordertest.RandomMatchID() 2418 matchTime := time.Now() 2419 extraMsgMatch := &msgjson.Match{ 2420 OrderID: oid[:], 2421 MatchID: extraID[:], 2422 Side: uint8(order.Taker), 2423 Status: uint8(order.MakerSwapCast), 2424 ServerTime: uint64(matchTime.UnixMilli()), 2425 } 2426 2427 // The extra match is already at MakerSwapCast, and we're the taker, which 2428 // will invoke match status conflict resolution and a contract audit. 2429 _, auditInfo := tMsgAudit(oid, extraID, addr, qty, encode.RandomBytes(32)) 2430 auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker)) 2431 tBtcWallet.auditInfo = auditInfo 2432 missedContract := encode.RandomBytes(50) 2433 rig.ws.queueResponse(msgjson.MatchStatusRoute, func(msg *msgjson.Message, f msgFunc) error { 2434 resp, _ := msgjson.NewResponse(msg.ID, []*msgjson.MatchStatusResult{{ 2435 MatchID: extraID[:], 2436 Status: uint8(order.MakerSwapCast), 2437 MakerContract: missedContract, 2438 MakerSwap: auditInfo.Coin.ID(), 2439 Active: true, 2440 MakerTxData: []byte{0x01}, 2441 }}, nil) 2442 f(resp) 2443 return nil 2444 }) 2445 2446 dc.trades = map[order.OrderID]*trackedTrade{ 2447 oid: tracker, 2448 } 2449 2450 tCore = rig.core 2451 rig.queueConnect(nil, []*msgjson.Match{knownMsgMatch /* missing missingMatch! */, extraMsgMatch}, nil) 2452 rig.queueCancel(nil) // for the unfunded order that gets canceled in authDEX 2453 // Login>authDEX will do 4 match DB updates for these two matches: 2454 // missing -> revoke -> update match 2455 // extra -> negotiate -> newTrackers -> update match 2456 // matchConflicts (from extras) -> resolveMatchConflicts -> resolveConflictWithServerData 2457 // -> update match after spawning auditContract 2458 // -> update match in auditContract (second because of lock) ** the ASYNC one we have to wait for ** 2459 rig.db.updateMatchChan = make(chan order.MatchStatus, 4) 2460 err = tCore.Login(tPW) // authDEX -> async contract audit for the extra match 2461 if err != nil || !rig.acct.authed() { 2462 t.Fatalf("final Login error: %v", err) 2463 } 2464 // Wait for expected db updates. 2465 for i := 0; i < 4; i++ { 2466 <-rig.db.updateMatchChan 2467 } 2468 2469 // check t.metaData.LinkedOrder or for db.LinkOrder call, then db.UpdateOrder call 2470 if tracker.metaData.LinkedOrder.IsZero() { 2471 t.Errorf("cancel order not set") 2472 } 2473 if rig.db.linkedFromID != oid || rig.db.linkedToID.IsZero() { 2474 t.Errorf("automatic cancel order not linked") 2475 } 2476 2477 if !tracker.matches[missingID].MetaData.Proof.SelfRevoked { 2478 t.Errorf("SelfRevoked not true for missing match tracker") 2479 } 2480 if tracker.matches[matchID].swapErr != nil { 2481 t.Errorf("swapErr set for non-missing match tracker") 2482 } 2483 if tracker.matches[matchID].MetaData.Proof.IsRevoked() { 2484 t.Errorf("IsRevoked true for non-missing match tracker") 2485 } 2486 // Conflict resolution will have run negotiate on the extra match from the 2487 // connect response, bringing our match count up to 3. 2488 if len(tracker.matches) != 3 { 2489 t.Errorf("Extra trade not accepted into matches") 2490 } 2491 tracker.mtx.Lock() 2492 defer tracker.mtx.Unlock() 2493 match = tracker.matches[extraID] 2494 if !bytes.Equal(match.MetaData.Proof.CounterContract, missedContract) { 2495 t.Errorf("Missed maker contract not retrieved, %s, %s", match, hex.EncodeToString(match.MetaData.Proof.CounterContract)) 2496 } 2497 } 2498 2499 func TestAccountNotFoundError(t *testing.T) { 2500 rig := newTestRig() 2501 defer rig.shutdown() 2502 tCore := rig.core 2503 rig.acct.rep = account.Reputation{BondedTier: 1} 2504 2505 const expectedErrorMessage = "test account not found error" 2506 accountNotFoundError := msgjson.NewError(msgjson.AccountNotFoundError, expectedErrorMessage) 2507 rig.queueConnect(accountNotFoundError, nil, nil) 2508 rig.queueConnect(accountNotFoundError, nil, nil) 2509 2510 wallet, _ := newTWallet(tUTXOAssetA.ID) 2511 tCore.wallets[tUTXOAssetA.ID] = wallet 2512 rig.queueConnect(nil, nil, nil) 2513 2514 feed := tCore.NotificationFeed() 2515 2516 tCore.initializeDEXConnections(rig.crypter) 2517 2518 // Make sure that the connections did not get authenticated 2519 for _, dc := range tCore.dexConnections() { 2520 if dc.acct.authed() { 2521 t.Fatalf("dex connection should not have been authenticated") 2522 } 2523 } 2524 2525 // Make sure that an error notification was sent 2526 for { 2527 select { 2528 case note := <-feed.C: 2529 if note.Topic() == TopicDexAuthError && strings.Contains(note.Details(), expectedErrorMessage) { 2530 return 2531 } 2532 case <-time.After(1 * time.Second): 2533 t.Fatalf("error notification could not be found") 2534 } 2535 } 2536 } 2537 2538 func TestInitializeDEXConnectionsSuccess(t *testing.T) { 2539 rig := newTestRig() 2540 defer rig.shutdown() 2541 tCore := rig.core 2542 rig.acct.rep = account.Reputation{BondedTier: 1} 2543 rig.queueConnect(nil, nil, nil) 2544 2545 // Make sure that the connections got authenticated 2546 tCore.initializeDEXConnections(rig.crypter) 2547 for _, dc := range tCore.dexConnections() { 2548 if !dc.acct.authed() { 2549 t.Fatalf("dex connection was not authenticated") 2550 } 2551 } 2552 } 2553 2554 func TestConnectDEX(t *testing.T) { 2555 rig := newTestRig() 2556 defer rig.shutdown() 2557 tCore := rig.core 2558 2559 ai := &db.AccountInfo{ 2560 Host: "somedex.com", 2561 } 2562 2563 _, err := tCore.connectDEX(ai) 2564 if err == nil { 2565 t.Fatalf("expected error for no TLS plain internet DEX host") 2566 } 2567 2568 ai.Host = "somedex13254214214.onion" // not a valid onion host in case we decide to validate them 2569 // No onion proxy set => error 2570 _, err = tCore.connectDEX(ai) 2571 if err == nil { 2572 t.Fatalf("expected error with no onion proxy set") 2573 } 2574 2575 rig.queueConfig() 2576 tCore.cfg.Onion = "127.0.0.1:9050" 2577 dc, err := tCore.connectDEX(ai) 2578 if err != nil { 2579 t.Fatalf("error connecting to onion host with an onion proxy configured: %v", err) 2580 } 2581 dc.connMaster.Disconnect() 2582 2583 rig.queueConfig() 2584 ai.Host = "somedex.com" 2585 ai.Cert = []byte{0x1} 2586 dc, err = tCore.connectDEX(ai) 2587 if err != nil { 2588 t.Fatalf("initial connectDEX error: %v", err) 2589 } 2590 dc.connMaster.Disconnect() 2591 2592 // Bad URL. 2593 ai.Host = tUnparseableHost // Illegal ASCII control character 2594 _, err = tCore.connectDEX(ai) 2595 if err == nil { 2596 t.Fatalf("no error for bad URL") 2597 } 2598 ai.Host = "someotherdex.org" 2599 2600 // Constructor error. 2601 ogConstructor := tCore.wsConstructor 2602 tCore.wsConstructor = func(*comms.WsCfg) (comms.WsConn, error) { 2603 return nil, tErr 2604 } 2605 _, err = tCore.connectDEX(ai) 2606 if err == nil { 2607 t.Fatalf("no error for WsConn constructor error") 2608 } 2609 tCore.wsConstructor = ogConstructor 2610 2611 // WsConn.Connect error. 2612 rig.ws.connectErr = tErr 2613 _, err = tCore.connectDEX(ai) 2614 if err == nil { 2615 t.Fatalf("no error for WsConn.Connect error") 2616 } 2617 2618 rig.ws.connectErr = nil 2619 2620 // 'config' route error. 2621 rig.ws.queueResponse(msgjson.ConfigRoute, func(msg *msgjson.Message, f msgFunc) error { 2622 resp, _ := msgjson.NewResponse(msg.ID, nil, msgjson.NewError(1, "test error")) 2623 f(resp) 2624 return nil 2625 }) 2626 _, err = tCore.connectDEX(ai) 2627 if err == nil { 2628 t.Fatalf("no error for 'config' route error") 2629 } 2630 2631 // Success again. 2632 rig.queueConfig() 2633 dc, err = tCore.connectDEX(ai) 2634 if err != nil { 2635 t.Fatalf("final connectDEX error: %v", err) 2636 } 2637 dc.connMaster.Disconnect() 2638 2639 // TODO: test temporary, ensure listen isn't running, somehow 2640 } 2641 2642 func TestInitializeClient(t *testing.T) { 2643 rig := newTestRig() 2644 defer rig.shutdown() 2645 tCore := rig.core 2646 2647 clearCreds := func() { 2648 tCore.credentials = nil 2649 rig.db.creds = nil 2650 } 2651 2652 clearCreds() 2653 2654 _, err := tCore.InitializeClient(tPW, nil) 2655 if err != nil { 2656 t.Fatalf("InitializeClient error: %v", err) 2657 } 2658 2659 clearCreds() 2660 2661 // Empty password. 2662 emptyPass := []byte("") 2663 _, err = tCore.InitializeClient(emptyPass, nil) 2664 if err == nil { 2665 t.Fatalf("no error for empty password") 2666 } 2667 2668 // Store error. Use a non-empty password to pass empty password check. 2669 rig.db.setCredsErr = tErr 2670 _, err = tCore.InitializeClient(tPW, nil) 2671 if err == nil { 2672 t.Fatalf("no error for StoreEncryptedKey error") 2673 } 2674 rig.db.setCredsErr = nil 2675 2676 // Success again 2677 _, err = tCore.InitializeClient(tPW, nil) 2678 if err != nil { 2679 t.Fatalf("final InitializeClient error: %v", err) 2680 } 2681 } 2682 2683 func TestSend(t *testing.T) { 2684 rig := newTestRig() 2685 defer rig.shutdown() 2686 tCore := rig.core 2687 wallet, tWallet := newTWallet(tUTXOAssetA.ID) 2688 tCore.wallets[tUTXOAssetA.ID] = wallet 2689 tWallet.sendCoin = &tCoin{id: encode.RandomBytes(36)} 2690 address := "addr" 2691 2692 // Successful 2693 coin, err := tCore.Send(tPW, tUTXOAssetA.ID, 1e8, address, false) 2694 if err != nil { 2695 t.Fatalf("Send error: %v", err) 2696 } 2697 if coin.Value() != 1e8 { 2698 t.Fatalf("Expected sent value to be %v, got %v", 1e8, coin.Value()) 2699 } 2700 2701 // 0 value 2702 _, err = tCore.Send(tPW, tUTXOAssetA.ID, 0, address, false) 2703 if err == nil { 2704 t.Fatalf("no error for zero value send") 2705 } 2706 2707 // no wallet 2708 _, err = tCore.Send(tPW, 12345, 1e8, address, false) 2709 if err == nil { 2710 t.Fatalf("no error for unknown wallet") 2711 } 2712 2713 // connect error 2714 wallet.hookedUp = false 2715 tWallet.connectErr = tErr 2716 _, err = tCore.Send(tPW, tUTXOAssetA.ID, 1e8, address, false) 2717 if err == nil { 2718 t.Fatalf("no error for wallet connect error") 2719 } 2720 tWallet.connectErr = nil 2721 2722 // Send error 2723 tWallet.sendErr = tErr 2724 _, err = tCore.Send(tPW, tUTXOAssetA.ID, 1e8, address, false) 2725 if err == nil { 2726 t.Fatalf("no error for wallet send error") 2727 } 2728 tWallet.sendErr = nil 2729 2730 // Check the coin. 2731 tWallet.sendCoin = &tCoin{id: []byte{'a'}} 2732 coin, err = tCore.Send(tPW, tUTXOAssetA.ID, 3e8, address, false) 2733 if err != nil { 2734 t.Fatalf("coin check error: %v", err) 2735 } 2736 coinID := coin.ID() 2737 if len(coinID) != 1 || coinID[0] != 'a' { 2738 t.Fatalf("coin ID not propagated") 2739 } 2740 if coin.Value() != 3e8 { 2741 t.Fatalf("Expected sent value to be %v, got %v", 3e8, coin.Value()) 2742 } 2743 2744 // So far, the fee suggestion should have always been zero. 2745 if tWallet.sendFeeSuggestion != 0 { 2746 t.Fatalf("unexpected non-zero fee rate when no books or responses prepared") 2747 } 2748 2749 const feeRate = 54321 2750 2751 feeRater := &TFeeRater{ 2752 TXCWallet: tWallet, 2753 feeRate: feeRate, 2754 } 2755 2756 wallet.Wallet = feeRater 2757 2758 coin, err = tCore.Send(tPW, tUTXOAssetA.ID, 2e8, address, false) 2759 if err != nil { 2760 t.Fatalf("FeeRater Withdraw/send error: %v", err) 2761 } 2762 if coin.Value() != 2e8 { 2763 t.Fatalf("Expected sent value to be %v, got %v", 2e8, coin.Value()) 2764 } 2765 2766 if tWallet.sendFeeSuggestion != feeRate { 2767 t.Fatalf("unexpected fee rate from FeeRater. wanted %d, got %d", feeRate, tWallet.sendFeeSuggestion) 2768 } 2769 2770 // wallet is not synced 2771 wallet.syncStatus.Synced = false 2772 _, err = tCore.Send(tPW, tUTXOAssetA.ID, 1e8, address, false) 2773 if err == nil { 2774 t.Fatalf("Expected error for a non-synchronized wallet") 2775 } 2776 } 2777 2778 func trade(t *testing.T, async bool) { 2779 rig := newTestRig() 2780 defer rig.shutdown() 2781 tCore := rig.core 2782 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 2783 dcrWallet.hookedUp = false 2784 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 2785 dcrWallet.address = "DsVmA7aqqWeKWy461hXjytbZbgCqbB8g2dq" 2786 dcrWallet.Unlock(rig.crypter) 2787 _ = dcrWallet.Connect() // connector will panic on Wait, and sync status goroutines will exit if disconnected 2788 defer dcrWallet.Disconnect() 2789 syncTickerPeriod = 10 * time.Millisecond 2790 2791 btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) 2792 tCore.wallets[tUTXOAssetB.ID] = btcWallet 2793 btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini" 2794 btcWallet.Unlock(rig.crypter) 2795 2796 ethWallet, tEthWallet := newTAccountLocker(tACCTAsset.ID) 2797 tCore.wallets[tACCTAsset.ID] = ethWallet 2798 ethWallet.address = "18d65fb8d60c1199bb1ad381be47aa692b482605" 2799 ethWallet.Unlock(rig.crypter) 2800 2801 var lots uint64 = 10 2802 qty := dcrBtcLotSize * lots 2803 rate := dcrBtcRateStep * 1000 2804 2805 form := &TradeForm{ 2806 Host: tDexHost, 2807 IsLimit: true, 2808 Sell: true, 2809 Base: tUTXOAssetA.ID, 2810 Quote: tUTXOAssetB.ID, 2811 Qty: qty, 2812 Rate: rate, 2813 TifNow: false, 2814 } 2815 2816 dcrCoin := &tCoin{ 2817 id: encode.RandomBytes(36), 2818 val: qty * 2, 2819 } 2820 tDcrWallet.fundingCoins = asset.Coins{dcrCoin} 2821 tDcrWallet.fundRedeemScripts = []dex.Bytes{nil} 2822 2823 btcVal := calc.BaseToQuote(rate, qty*2) 2824 btcCoin := &tCoin{ 2825 id: encode.RandomBytes(36), 2826 val: btcVal, 2827 } 2828 tBtcWallet.fundingCoins = asset.Coins{btcCoin} 2829 tBtcWallet.fundRedeemScripts = []dex.Bytes{nil} 2830 2831 book := newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger) 2832 rig.dc.books[tDcrBtcMktName] = book 2833 2834 msgOrderNote := &msgjson.BookOrderNote{ 2835 OrderNote: msgjson.OrderNote{ 2836 OrderID: encode.RandomBytes(32), 2837 }, 2838 TradeNote: msgjson.TradeNote{ 2839 Side: msgjson.SellOrderNum, 2840 Quantity: dcrBtcLotSize, 2841 Time: uint64(time.Now().Unix()), 2842 Rate: rate, 2843 }, 2844 } 2845 2846 err := book.Sync(&msgjson.OrderBook{ 2847 MarketID: tDcrBtcMktName, 2848 Seq: 1, 2849 Epoch: 1, 2850 Orders: []*msgjson.BookOrderNote{msgOrderNote}, 2851 }) 2852 if err != nil { 2853 t.Fatalf("order book sync error: %v", err) 2854 } 2855 2856 badSig := false 2857 noID := false 2858 badID := false 2859 handleLimit := func(msg *msgjson.Message, f msgFunc) error { 2860 t.Helper() 2861 // Need to stamp and sign the message with the server's key. 2862 msgOrder := new(msgjson.LimitOrder) 2863 err := msg.Unmarshal(msgOrder) 2864 if err != nil { 2865 t.Fatalf("unmarshal error: %v", err) 2866 } 2867 lo := convertMsgLimitOrder(msgOrder) 2868 f(orderResponse(msg.ID, msgOrder, lo, badSig, noID, badID)) 2869 return nil 2870 } 2871 2872 handleMarket := func(msg *msgjson.Message, f msgFunc) error { 2873 t.Helper() 2874 // Need to stamp and sign the message with the server's key. 2875 msgOrder := new(msgjson.MarketOrder) 2876 err := msg.Unmarshal(msgOrder) 2877 if err != nil { 2878 t.Fatalf("unmarshal error: %v", err) 2879 } 2880 mo := convertMsgMarketOrder(msgOrder) 2881 f(orderResponse(msg.ID, msgOrder, mo, badSig, noID, badID)) 2882 return nil 2883 } 2884 2885 ch := tCore.NotificationFeed() // detect when sync goroutine completes 2886 waitForOrderNotification := func() (*Order, uint64, error) { 2887 var corder *Order 2888 var tempID uint64 2889 wait: 2890 for { 2891 select { 2892 case note := <-ch.C: 2893 if note.Type() == NoteTypeOrder { 2894 n, ok := note.(*OrderNote) 2895 if !ok { 2896 t.Fatalf("Expected OrderNote type, got %T", note) 2897 } 2898 if note.Topic() == TopicAsyncOrderSubmitted { 2899 tempID = n.TemporaryID 2900 } else if tempID == n.TemporaryID && note.Topic() == TopicAsyncOrderFailure { 2901 return nil, tempID, fmt.Errorf("%v", note.Details()) 2902 } else { 2903 corder = n.Order 2904 break wait 2905 } 2906 } 2907 case <-time.After(1 * time.Second): 2908 t.Fatal("Failed to receive queued order note") 2909 } 2910 } 2911 return corder, tempID, nil 2912 } 2913 2914 trade := func() (*Order, error) { 2915 if !async { 2916 return tCore.Trade(tPW, form) 2917 } 2918 2919 inFlight, err := tCore.TradeAsync(tPW, form) 2920 if err != nil { 2921 return nil, err 2922 } 2923 2924 corder, tempID, err := waitForOrderNotification() 2925 if err != nil { 2926 return nil, err 2927 } 2928 2929 if inFlight.TemporaryID != tempID { 2930 t.Fatalf("received wrong in-flight order, expected %d got %d", inFlight.TemporaryID, tempID) 2931 } 2932 2933 return corder, nil 2934 } 2935 2936 ensureOrderErr := func(tag string, waitForErr bool) { 2937 t.Helper() 2938 var err error 2939 if async { 2940 _, err = tCore.TradeAsync(tPW, form) 2941 } else { 2942 _, err = tCore.Trade(tPW, form) 2943 } 2944 if !waitForErr && err == nil { 2945 t.Fatalf("%s: no error", tag) 2946 } 2947 2948 if waitForErr { 2949 _, _, err := waitForOrderNotification() 2950 if err == nil { 2951 t.Fatalf("%s: no error for queued order", tag) 2952 } 2953 } 2954 } 2955 2956 ensureErr := func(tag string) { 2957 t.Helper() 2958 ensureOrderErr(tag, false) 2959 } 2960 2961 // Initial success 2962 rig.ws.queueResponse(msgjson.LimitRoute, handleLimit) 2963 corder, err := trade() 2964 if err != nil { 2965 t.Fatalf("limit order error: %v", err) 2966 } 2967 t.Logf("Order with ID(%s) has been placed successfully!", corder.ID.String()) 2968 2969 // Check that the Fund request for a limit sell came through and that 2970 // value was not adjusted internally with BaseToQuote. 2971 if tDcrWallet.fundedVal != qty { 2972 t.Fatalf("limit sell expected funded value %d, got %d", qty, tDcrWallet.fundedVal) 2973 } 2974 tDcrWallet.fundedVal = 0 2975 if tDcrWallet.fundedSwaps != lots { 2976 t.Fatalf("limit sell expected %d max swaps, got %d", lots, tDcrWallet.fundedSwaps) 2977 } 2978 tDcrWallet.fundedSwaps = 0 2979 2980 // Should not be able to close wallet now, since there are orders. 2981 if tCore.CloseWallet(tUTXOAssetA.ID) == nil { 2982 t.Fatalf("no error for closing DCR wallet with active orders") 2983 } 2984 if tCore.CloseWallet(tUTXOAssetB.ID) == nil { 2985 t.Fatalf("no error for closing BTC wallet with active orders") 2986 } 2987 2988 // Should not be able to disable wallet, since there are active orders. 2989 if tCore.ToggleWalletStatus(tUTXOAssetA.ID, true) == nil { 2990 t.Fatalf("no error for disabling DCR wallet with active orders") 2991 } 2992 if tCore.ToggleWalletStatus(tUTXOAssetB.ID, true) == nil { 2993 t.Fatalf("no error for disabling BTC wallet with active orders") 2994 } 2995 2996 // We want to set peerCount to 0 (from 1), but we'll do this the hard way to 2997 // ensure the peerChange handler works as intended. 2998 // dcrWallet.mtx.Lock() 2999 // dcrWallet.peerCount = 0 3000 // dcrWallet.mtx.Unlock() 3001 tCore.peerChange(dcrWallet, 0, nil) 3002 _, err = tCore.Trade(tPW, form) 3003 if err == nil { 3004 t.Fatalf("no error for no peers") 3005 } 3006 tCore.peerChange(dcrWallet, 1, nil) 3007 3008 // Dex not found 3009 form.Host = "someotherdex.org" 3010 _, err = tCore.Trade(tPW, form) 3011 if err == nil { 3012 t.Fatalf("no error for unknown dex") 3013 } 3014 form.Host = tDexHost 3015 3016 // Account locked = probably not logged in 3017 rig.dc.acct.lock() 3018 _, err = tCore.Trade(tPW, form) 3019 if err == nil { 3020 t.Fatalf("no error for disconnected dex") 3021 } 3022 rig.dc.acct.unlock(rig.crypter) 3023 3024 // DEX not connected 3025 atomic.StoreUint32(&rig.dc.connectionStatus, uint32(comms.Disconnected)) 3026 _, err = tCore.Trade(tPW, form) 3027 if err == nil { 3028 t.Fatalf("no error for disconnected dex") 3029 } 3030 atomic.StoreUint32(&rig.dc.connectionStatus, uint32(comms.Connected)) 3031 3032 setWalletSyncStatus := func(w *xcWallet, status bool) { 3033 w.mtx.Lock() 3034 w.syncStatus.Synced = status 3035 w.mtx.Unlock() 3036 } 3037 3038 // No base asset 3039 form.Base = 12345 3040 ensureErr("bad base asset") 3041 form.Base = tUTXOAssetA.ID 3042 3043 // No quote asset 3044 form.Quote = 12345 3045 ensureErr("bad quote asset") 3046 form.Quote = tUTXOAssetB.ID 3047 3048 // Limit order zero rate 3049 form.Rate = 0 3050 ensureErr("zero rate limit") 3051 form.Rate = rate 3052 3053 // No from wallet 3054 tCore.walletMtx.Lock() 3055 delete(tCore.wallets, tUTXOAssetA.ID) 3056 tCore.walletMtx.Unlock() 3057 ensureErr("no dcr wallet") 3058 tCore.walletMtx.Lock() 3059 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 3060 tCore.walletMtx.Unlock() 3061 3062 // No to wallet 3063 tCore.walletMtx.Lock() 3064 delete(tCore.wallets, tUTXOAssetB.ID) 3065 tCore.walletMtx.Unlock() 3066 ensureErr("no btc wallet") 3067 tCore.walletMtx.Lock() 3068 tCore.wallets[tUTXOAssetB.ID] = btcWallet 3069 tCore.walletMtx.Unlock() 3070 3071 // Address error 3072 tBtcWallet.addrErr = tErr 3073 ensureErr("address error") 3074 tBtcWallet.addrErr = nil 3075 3076 // Not enough funds 3077 tDcrWallet.fundingCoinErr = tErr 3078 ensureErr("funds error") 3079 tDcrWallet.fundingCoinErr = nil 3080 3081 // Lot size violation 3082 ogQty := form.Qty 3083 form.Qty += dcrBtcLotSize / 2 3084 ensureErr("bad size") 3085 form.Qty = ogQty 3086 3087 // Coin signature error 3088 tDcrWallet.signCoinErr = tErr 3089 ensureErr("signature error") 3090 tDcrWallet.signCoinErr = nil 3091 3092 // Sync-in-progress error 3093 setWalletSyncStatus(dcrWallet, false) 3094 ensureErr("base not synced") 3095 setWalletSyncStatus(dcrWallet, true) 3096 3097 setWalletSyncStatus(btcWallet, false) 3098 ensureErr("quote not synced") 3099 setWalletSyncStatus(btcWallet, true) 3100 3101 // LimitRoute error 3102 rig.ws.reqErr = tErr 3103 ensureOrderErr("Request error", async) 3104 rig.ws.reqErr = nil 3105 3106 // The rest need a queued handler 3107 3108 // Bad signature 3109 rig.ws.queueResponse(msgjson.LimitRoute, handleLimit) 3110 badSig = true 3111 ensureOrderErr("bad server sig", async) 3112 badSig = false 3113 3114 // No order ID in response 3115 rig.ws.queueResponse(msgjson.LimitRoute, handleLimit) 3116 noID = true 3117 ensureOrderErr("no ID", async) 3118 noID = false 3119 3120 // Wrong order ID in response 3121 rig.ws.queueResponse(msgjson.LimitRoute, handleLimit) 3122 badID = true 3123 ensureOrderErr("no ID", async) 3124 badID = false 3125 3126 // Storage failure 3127 rig.ws.queueResponse(msgjson.LimitRoute, handleLimit) 3128 rig.db.updateOrderErr = tErr 3129 ensureOrderErr("db failure", async) 3130 rig.db.updateOrderErr = nil 3131 3132 // Success when buying. 3133 form.Sell = false 3134 rig.ws.queueResponse(msgjson.LimitRoute, handleLimit) 3135 corder, err = trade() 3136 if err != nil { 3137 t.Fatalf("limit order error: %v", err) 3138 } 3139 t.Logf("Order with ID(%s) has been placed successfully!", corder.ID.String()) 3140 3141 // Check that the Fund request for a limit buy came through to the BTC wallet 3142 // and that the value was adjusted internally with BaseToQuote. 3143 expQty := calc.BaseToQuote(rate, qty) 3144 if tBtcWallet.fundedVal != expQty { 3145 t.Fatalf("limit buy expected funded value %d, got %d", expQty, tBtcWallet.fundedVal) 3146 } 3147 tBtcWallet.fundedVal = 0 3148 // The number of lots should still be the same as for a sell order. 3149 if tBtcWallet.fundedSwaps != lots { 3150 t.Fatalf("limit buy expected %d max swaps, got %d", lots, tBtcWallet.fundedSwaps) 3151 } 3152 tBtcWallet.fundedSwaps = 0 3153 3154 // Successful market buy order 3155 form.IsLimit = false 3156 form.Qty = calc.BaseToQuote(rate, qty) 3157 rig.ws.queueResponse(msgjson.MarketRoute, handleMarket) 3158 corder, err = trade() 3159 if err != nil { 3160 t.Fatalf("market order error: %v", err) 3161 } 3162 t.Logf("Order with ID(%s) has been placed successfully!", corder.ID.String()) 3163 3164 // The funded qty for a market buy should not be adjusted. 3165 if tBtcWallet.fundedVal != form.Qty { 3166 t.Fatalf("market buy expected funded value %d, got %d", qty, tBtcWallet.fundedVal) 3167 } 3168 tBtcWallet.fundedVal = 0 3169 if tBtcWallet.fundedSwaps != lots { 3170 t.Fatalf("market buy expected %d max swaps, got %d", lots, tBtcWallet.fundedSwaps) 3171 } 3172 tBtcWallet.fundedSwaps = 0 3173 3174 // Successful market sell order. 3175 form.Sell = true 3176 form.Qty = qty 3177 rig.ws.queueResponse(msgjson.MarketRoute, handleMarket) 3178 corder, err = trade() 3179 if err != nil { 3180 t.Fatalf("market order error: %v", err) 3181 } 3182 t.Logf("Order with ID(%s) has been placed successfully!", corder.ID.String()) 3183 3184 // The funded qty for a market sell order should not be adjusted. 3185 if tDcrWallet.fundedVal != qty { 3186 t.Fatalf("market sell expected funded value %d, got %d", qty, tDcrWallet.fundedVal) 3187 } 3188 if tDcrWallet.fundedSwaps != lots { 3189 t.Fatalf("market sell expected %d max swaps, got %d", lots, tDcrWallet.fundedSwaps) 3190 } 3191 3192 // Selling to an account-based quote asset. 3193 const reserveN = 50 3194 form.Base = tUTXOAssetB.ID 3195 form.Quote = tACCTAsset.ID 3196 rig.ws.queueResponse(msgjson.MarketRoute, handleMarket) 3197 tEthWallet.fundingMtx.Lock() 3198 tEthWallet.reserveNRedemptions = reserveN 3199 tEthWallet.fundingMtx.Unlock() 3200 tEthWallet.sigs = []dex.Bytes{{}} 3201 tEthWallet.pubKeys = []dex.Bytes{{}} 3202 corder, err = trade() 3203 if err != nil { 3204 t.Fatalf("account-redeemed order error: %v", err) 3205 } 3206 t.Logf("Order with ID(%s) has been placed successfully!", corder.ID.String()) 3207 3208 // redeem sig error 3209 tEthWallet.signCoinErr = tErr 3210 ensureErr("redeem sig error") 3211 tEthWallet.signCoinErr = nil 3212 3213 // missing sig 3214 tEthWallet.sigs = []dex.Bytes{} 3215 ensureErr("no redeem sig is result") 3216 tEthWallet.sigs = []dex.Bytes{{}} 3217 3218 // ReserveN error 3219 tEthWallet.reserveNRedemptionsErr = tErr 3220 ensureErr("reserveN error") 3221 tEthWallet.reserveNRedemptionsErr = nil 3222 3223 // Funds returned for later error. 3224 tEthWallet.fundingMtx.Lock() 3225 tEthWallet.redemptionUnlocked = 0 3226 tEthWallet.fundingMtx.Unlock() 3227 rig.db.updateOrderErr = tErr 3228 rig.ws.queueResponse(msgjson.MarketRoute, handleMarket) 3229 ensureOrderErr("db error after redeem funds checked out", async) 3230 rig.db.updateOrderErr = nil 3231 tEthWallet.fundingMtx.Lock() 3232 defer tEthWallet.fundingMtx.Unlock() 3233 if tEthWallet.redemptionUnlocked != reserveN { 3234 t.Fatalf("redeem funds not returned") 3235 } 3236 } 3237 3238 func TestTrade(t *testing.T) { 3239 trade(t, false) 3240 } 3241 3242 func TestTradeAsync(t *testing.T) { 3243 trade(t, true) 3244 } 3245 3246 func TestRefundReserves(t *testing.T) { 3247 const reserves = 100_000 3248 3249 rig := newTestRig() 3250 defer rig.shutdown() 3251 dc := rig.dc 3252 tCore := rig.core 3253 3254 btcWallet, _ := newTWallet(tUTXOAssetA.ID) 3255 tCore.wallets[tUTXOAssetA.ID] = btcWallet 3256 btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini" 3257 btcWallet.Unlock(rig.crypter) 3258 3259 ethWallet, tEthWallet := newTAccountLocker(tACCTAsset.ID) 3260 tCore.wallets[tACCTAsset.ID] = ethWallet 3261 ethWallet.address = "18d65fb8d60c1199bb1ad381be47aa692b482605" 3262 ethWallet.Unlock(rig.crypter) 3263 3264 lotSize := dcrBtcLotSize 3265 qty := lotSize * 10 3266 rate := dcrBtcRateStep * 100 3267 3268 lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, qty, rate) 3269 lo.BaseAsset = tUTXOAssetB.ID 3270 lo.QuoteAsset = tACCTAsset.ID 3271 lo.Force = order.StandingTiF 3272 loid := lo.ID() 3273 3274 walletSet, _, _, err := tCore.walletSet(dc, tACCTAsset.ID, tUTXOAssetA.ID, true) 3275 if err != nil { 3276 t.Fatalf("walletSet error: %v", err) 3277 } 3278 3279 dbOrder.MetaData.RefundReserves = reserves 3280 3281 tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 3282 rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails) 3283 dc.trades[loid] = tracker 3284 preImgC := newPreimage() 3285 co := &order.CancelOrder{ 3286 P: order.Prefix{ 3287 AccountID: dc.acct.ID(), 3288 BaseAsset: tACCTAsset.ID, 3289 QuoteAsset: tUTXOAssetA.ID, 3290 OrderType: order.MarketOrderType, 3291 ClientTime: time.Now(), 3292 ServerTime: time.Now().Add(time.Millisecond), 3293 Commit: preImgC.Commit(), 3294 }, 3295 } 3296 3297 msgCancelMatch := &msgjson.Match{ 3298 OrderID: loid[:], 3299 MatchID: encode.RandomBytes(32), 3300 Quantity: qty / 3, 3301 // empty Address signals cancel order match 3302 } 3303 sign(tDexPriv, msgCancelMatch) 3304 3305 matchQty := qty * 2 / 3 3306 matchReserves := applyFraction(2, 3, reserves) 3307 msgMatch := &msgjson.Match{ 3308 OrderID: loid[:], 3309 MatchID: encode.RandomBytes(32), 3310 Quantity: matchQty, 3311 Rate: rate, 3312 Address: "somenonemptyaddress", 3313 } 3314 sign(tDexPriv, msgMatch) 3315 3316 test := func(tag string, expUnlock uint64, f func()) { 3317 t.Helper() 3318 tEthWallet.refundUnlocked = 0 3319 tracker.refundLocked = reserves 3320 tracker.metaData.Status = order.OrderStatusEpoch 3321 f() 3322 if tEthWallet.refundUnlocked != expUnlock { 3323 t.Fatalf("%s: expected %d to be unlocked. saw %d", tag, expUnlock, tEthWallet.refundUnlocked) 3324 } 3325 } 3326 3327 test("revoke_order in epoch", reserves, func() { 3328 tracker.revoke() 3329 }) 3330 3331 test("revoke_order in booked, partial fill", reserves/2, func() { 3332 // Revoke in booked with partial fill. 3333 tracker.Trade().SetFill(qty / 2) 3334 tracker.metaData.Status = order.OrderStatusBooked 3335 tracker.revoke() 3336 }) 3337 3338 test("canceled, partially filled", reserves/3, func() { 3339 tracker.cancel = &trackedCancel{CancelOrder: *co} 3340 msgCancel, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch, msgCancelMatch}) 3341 if err := handleMatchRoute(tCore, rig.dc, msgCancel); err != nil { 3342 t.Fatalf("handleMatchRoute error: %v", err) 3343 } 3344 }) 3345 3346 tracker.cancel = nil 3347 3348 lo.Force = order.ImmediateTiF 3349 loid = lo.ID() 3350 msgMatch.OrderID = loid[:] 3351 sign(tDexPriv, msgMatch) 3352 3353 test("partial immediate TiF limit order", reserves/3, func() { 3354 matchReq, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch}) 3355 if err := handleMatchRoute(tCore, rig.dc, matchReq); err != nil { 3356 t.Fatalf("handleMatchRoute error: %v", err) 3357 } 3358 }) 3359 3360 lo.Force = order.StandingTiF 3361 loid = lo.ID() 3362 msgMatch.OrderID = loid[:] 3363 sign(tDexPriv, msgMatch) 3364 3365 addMatch := func(side order.MatchSide, status order.MatchStatus, qty uint64) order.MatchID { 3366 t.Helper() 3367 msgMatch.Side = uint8(side) 3368 m := *msgMatch 3369 var mid order.MatchID 3370 copy(mid[:], encode.RandomBytes(32)) 3371 m.MatchID = mid[:] 3372 m.Quantity = qty 3373 sign(tDexPriv, &m) 3374 matchReq, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{&m}) 3375 if err := handleMatchRoute(tCore, rig.dc, matchReq); err != nil { 3376 t.Fatalf("handleMatchRoute error: %v", err) 3377 } 3378 mt, ok := tracker.matches[mid] 3379 if !ok { 3380 t.Fatalf("match not found") 3381 } 3382 mt.Status = status 3383 if status >= order.TakerSwapCast { 3384 mt.counterSwap = &asset.AuditInfo{} 3385 } 3386 return mid 3387 } 3388 3389 resetMatches := func() { 3390 tracker.matches = make(map[order.MatchID]*matchTracker) 3391 } 3392 3393 test("redemption received", reserves/10, func() { 3394 mid := addMatch(order.Taker, order.TakerSwapCast, lotSize) 3395 redemption := &msgjson.Redemption{ 3396 Redeem: msgjson.Redeem{ 3397 OrderID: loid[:], 3398 MatchID: mid[:], 3399 CoinID: encode.RandomBytes(36), 3400 }, 3401 } 3402 tracker.processRedemption(1, redemption) 3403 }) 3404 3405 // Market sell order 3406 mo := &order.MarketOrder{ 3407 P: lo.P, 3408 T: *lo.Trade(), 3409 } 3410 mo.Prefix().OrderType = order.MarketOrderType 3411 moid := mo.ID() 3412 dbOrder.Order = mo 3413 msgMatch.OrderID = moid[:] 3414 sign(tDexPriv, msgMatch) 3415 3416 tracker = newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 3417 rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails) 3418 dc.trades = map[order.OrderID]*trackedTrade{moid: tracker} 3419 3420 test("nomatch", reserves, func() { 3421 tracker.nomatch(moid) 3422 }) 3423 3424 test("partial market sell match", reserves/3, func() { 3425 matchReq, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch}) 3426 if err := handleMatchRoute(tCore, rig.dc, matchReq); err != nil { 3427 t.Fatalf("handleMatchRoute error: %v", err) 3428 } 3429 }) 3430 3431 resetMatches() 3432 3433 testRevokeMatch := func(side order.MatchSide, status order.MatchStatus, expReserves uint64) { 3434 t.Helper() 3435 resetMatches() 3436 matchID := addMatch(side, status, matchQty) 3437 desc := fmt.Sprintf("match revoke - %s in %s", side, status) 3438 test(desc, expReserves, func() { 3439 tracker.revokeMatch(matchID, true) 3440 }) 3441 } 3442 3443 testRevokeMatch(order.Maker, order.NewlyMatched, matchReserves) 3444 3445 testRevokeMatch(order.Taker, order.NewlyMatched, matchReserves) 3446 3447 testRevokeMatch(order.Taker, order.MakerSwapCast, matchReserves) 3448 3449 // But Maker in MakerSwapCast shouldn't return reserves, because they will 3450 // need to do a refund 3451 testRevokeMatch(order.Maker, order.MakerSwapCast, 0) 3452 3453 // Similarly Taker in TakerSwapCast shouldn't return anything, since 3454 // they will need to do a refund. 3455 testRevokeMatch(order.Taker, order.TakerSwapCast, 0) 3456 3457 resetMatches() 3458 3459 // Market buy order 3460 mo.BaseAsset, mo.QuoteAsset = mo.QuoteAsset, mo.BaseAsset 3461 mo.Sell = false 3462 tracker.wallets, _, _, _ = tCore.walletSet(dc, tUTXOAssetA.ID, tACCTAsset.ID, false) 3463 3464 test("redemption received, market buy", reserves, func() { 3465 mid := addMatch(order.Taker, order.TakerSwapCast, lotSize) 3466 redemption := &msgjson.Redemption{ 3467 Redeem: msgjson.Redeem{ 3468 OrderID: loid[:], 3469 MatchID: mid[:], 3470 CoinID: encode.RandomBytes(36), 3471 }, 3472 } 3473 tracker.processRedemption(1, redemption) 3474 }) 3475 3476 resetMatches() 3477 mids := []order.MatchID{ 3478 addMatch(order.Maker, order.NewlyMatched, lotSize*2), 3479 addMatch(order.Maker, order.NewlyMatched, lotSize*2), 3480 addMatch(order.Maker, order.NewlyMatched, lotSize*2), 3481 } 3482 3483 tracker.refundLocked = reserves 3484 tEthWallet.refundUnlocked = 0 3485 for _, mid := range mids { 3486 // Third match should catch the market buy order dust filter. 3487 if err := tracker.revokeMatch(mid, true); err != nil { 3488 t.Fatalf("revokeMatch error: %v", err) 3489 } 3490 } 3491 if tracker.refundLocked != 0 { 3492 t.Fatalf("redemptionLocked (1/3) * 3 != 1: %d still reserved of %d", tracker.refundLocked, reserves) 3493 } 3494 if tEthWallet.refundUnlocked != reserves { 3495 t.Fatalf("redemptionUnlocked (1/3) * 3 != 1: %d returned of %d", tEthWallet.refundUnlocked, reserves) 3496 } 3497 } 3498 3499 func TestRedemptionReserves(t *testing.T) { 3500 const reserves = 100_000 3501 3502 rig := newTestRig() 3503 defer rig.shutdown() 3504 dc := rig.dc 3505 tCore := rig.core 3506 3507 btcWallet, _ := newTWallet(tUTXOAssetB.ID) 3508 tCore.wallets[tUTXOAssetB.ID] = btcWallet 3509 btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini" 3510 btcWallet.Unlock(rig.crypter) 3511 3512 ethWallet, tEthWallet := newTAccountLocker(tACCTAsset.ID) 3513 tCore.wallets[tACCTAsset.ID] = ethWallet 3514 ethWallet.address = "18d65fb8d60c1199bb1ad381be47aa692b482605" 3515 ethWallet.Unlock(rig.crypter) 3516 3517 lotSize := dcrBtcLotSize 3518 qty := lotSize * 10 3519 rate := dcrBtcRateStep * 100 3520 3521 lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, qty, rate) 3522 lo.BaseAsset = tUTXOAssetB.ID 3523 lo.QuoteAsset = tACCTAsset.ID 3524 lo.Force = order.StandingTiF 3525 loid := lo.ID() 3526 3527 walletSet, _, _, err := tCore.walletSet(dc, tUTXOAssetB.ID, tACCTAsset.ID, true) 3528 if err != nil { 3529 t.Fatalf("walletSet error: %v", err) 3530 } 3531 3532 dbOrder.MetaData.RedemptionReserves = reserves 3533 3534 tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 3535 rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails) 3536 dc.trades[loid] = tracker 3537 preImgC := newPreimage() 3538 co := &order.CancelOrder{ 3539 P: order.Prefix{ 3540 AccountID: dc.acct.ID(), 3541 BaseAsset: tACCTAsset.ID, 3542 QuoteAsset: tUTXOAssetB.ID, 3543 OrderType: order.MarketOrderType, 3544 ClientTime: time.Now(), 3545 ServerTime: time.Now().Add(time.Millisecond), 3546 Commit: preImgC.Commit(), 3547 }, 3548 } 3549 3550 msgCancelMatch := &msgjson.Match{ 3551 OrderID: loid[:], 3552 MatchID: encode.RandomBytes(32), 3553 Quantity: qty / 3, 3554 // empty Address signals cancel order match 3555 } 3556 sign(tDexPriv, msgCancelMatch) 3557 3558 matchQty := qty * 2 / 3 3559 matchReserves := applyFraction(2, 3, reserves) 3560 msgMatch := &msgjson.Match{ 3561 OrderID: loid[:], 3562 MatchID: encode.RandomBytes(32), 3563 Quantity: matchQty, 3564 Rate: rate, 3565 Address: "somenonemptyaddress", 3566 } 3567 sign(tDexPriv, msgMatch) 3568 3569 test := func(tag string, expUnlock uint64, f func()) { 3570 t.Helper() 3571 tEthWallet.redemptionUnlocked = 0 3572 tracker.redemptionLocked = reserves 3573 tracker.metaData.Status = order.OrderStatusEpoch 3574 f() 3575 if tEthWallet.redemptionUnlocked != expUnlock { 3576 t.Fatalf("%s: expected %d to be unlocked. saw %d", tag, expUnlock, tEthWallet.redemptionUnlocked) 3577 } 3578 } 3579 3580 test("revoke_order in epoch", reserves, func() { 3581 tracker.revoke() 3582 }) 3583 3584 test("revoke_order in booked, partial fill", reserves/2, func() { 3585 // Revoke in booked with partial fill. 3586 tracker.Trade().SetFill(qty / 2) 3587 tracker.metaData.Status = order.OrderStatusBooked 3588 tracker.revoke() 3589 }) 3590 3591 test("canceled, partially filled", reserves/3, func() { 3592 tracker.cancel = &trackedCancel{CancelOrder: *co} 3593 msgCancel, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch, msgCancelMatch}) 3594 if err := handleMatchRoute(tCore, rig.dc, msgCancel); err != nil { 3595 t.Fatalf("handleMatchRoute error: %v", err) 3596 } 3597 }) 3598 3599 tracker.cancel = nil 3600 3601 lo.Force = order.ImmediateTiF 3602 loid = lo.ID() 3603 msgMatch.OrderID = loid[:] 3604 sign(tDexPriv, msgMatch) 3605 3606 test("partially filled immediate TiF limit order", reserves/3, func() { 3607 matchReq, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch}) 3608 if err := handleMatchRoute(tCore, rig.dc, matchReq); err != nil { 3609 t.Fatalf("handleMatchRoute error: %v", err) 3610 } 3611 }) 3612 3613 mo := &order.MarketOrder{ 3614 P: lo.P, 3615 T: *lo.Trade(), 3616 } 3617 mo.Prefix().OrderType = order.MarketOrderType 3618 moid := mo.ID() 3619 dbOrder.Order = mo 3620 msgMatch.OrderID = moid[:] 3621 sign(tDexPriv, msgMatch) 3622 3623 tracker = newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 3624 rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails) 3625 dc.trades = map[order.OrderID]*trackedTrade{moid: tracker} 3626 3627 test("nomatch", reserves, func() { 3628 tracker.nomatch(moid) 3629 }) 3630 3631 test("partial market sell match", reserves/3, func() { 3632 matchReq, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch}) 3633 if err := handleMatchRoute(tCore, rig.dc, matchReq); err != nil { 3634 t.Fatalf("handleMatchRoute error: %v", err) 3635 } 3636 }) 3637 3638 addMatch := func(side order.MatchSide, status order.MatchStatus, qty uint64) order.MatchID { 3639 msgMatch.Side = uint8(side) 3640 m := *msgMatch 3641 var mid order.MatchID 3642 copy(mid[:], encode.RandomBytes(32)) 3643 m.MatchID = mid[:] 3644 m.Quantity = qty 3645 sign(tDexPriv, &m) 3646 matchReq, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{&m}) 3647 if err := handleMatchRoute(tCore, rig.dc, matchReq); err != nil { 3648 t.Fatalf("handleMatchRoute error: %v", err) 3649 } 3650 mt, ok := tracker.matches[mid] 3651 if !ok { 3652 t.Fatalf("match not found") 3653 } 3654 mt.Status = status 3655 return mid 3656 } 3657 3658 resetMatches := func() { 3659 tracker.matches = make(map[order.MatchID]*matchTracker) 3660 } 3661 3662 testRevokeMatch := func(side order.MatchSide, status order.MatchStatus, expReserves uint64) { 3663 t.Helper() 3664 resetMatches() 3665 matchID := addMatch(side, status, matchQty) 3666 desc := fmt.Sprintf("match revoke - %s in %s", side, status) 3667 test(desc, expReserves, func() { 3668 tracker.revokeMatch(matchID, true) 3669 }) 3670 } 3671 3672 testRevokeMatch(order.Maker, order.NewlyMatched, matchReserves) 3673 3674 testRevokeMatch(order.Taker, order.NewlyMatched, matchReserves) 3675 3676 testRevokeMatch(order.Taker, order.MakerSwapCast, matchReserves) 3677 3678 // But Maker in MakerSwapCast shouldn't return reserves, since the trade 3679 // will proceed to redeem. 3680 testRevokeMatch(order.Maker, order.MakerSwapCast, 0) 3681 3682 // Similarly Taker in TakerSwapCast shouldn't return anything, since we will 3683 // be watching for a redemption. 3684 testRevokeMatch(order.Taker, order.TakerSwapCast, 0) 3685 3686 // Market buy order with dust handling. 3687 mo.BaseAsset, mo.QuoteAsset = mo.QuoteAsset, mo.BaseAsset 3688 mo.Sell = false 3689 tracker.wallets, _, _, _ = tCore.walletSet(dc, tACCTAsset.ID, tUTXOAssetB.ID, false) 3690 3691 resetMatches() 3692 mids := []order.MatchID{ 3693 addMatch(order.Maker, order.NewlyMatched, lotSize*2), 3694 addMatch(order.Maker, order.NewlyMatched, lotSize*2), 3695 addMatch(order.Maker, order.NewlyMatched, lotSize*2), 3696 } 3697 3698 tracker.redemptionLocked = reserves 3699 tEthWallet.redemptionUnlocked = 0 3700 for _, mid := range mids { 3701 // Third match should catch the market buy order dust filter. 3702 if err := tracker.revokeMatch(mid, true); err != nil { 3703 t.Fatalf("revokeMatch error: %v", err) 3704 } 3705 } 3706 if tracker.redemptionLocked != 0 { 3707 t.Fatalf("redemptionLocked (1/3) * 3 != 1: %d still reserved of %d", tracker.redemptionLocked, reserves) 3708 } 3709 if tEthWallet.redemptionUnlocked != reserves { 3710 t.Fatalf("redemptionUnlocked (1/3) * 3 != 1: %d returned of %d", tEthWallet.redemptionUnlocked, reserves) 3711 } 3712 } 3713 3714 func TestCancel(t *testing.T) { 3715 rig := newTestRig() 3716 defer rig.shutdown() 3717 dc := rig.dc 3718 lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, 0, 0) 3719 lo.Force = order.StandingTiF 3720 oid := lo.ID() 3721 tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 3722 rig.db, rig.queue, nil, nil, rig.core.notify, rig.core.formatDetails) 3723 dc.trades[oid] = tracker 3724 3725 rig.queueCancel(nil) 3726 err := rig.core.Cancel(oid[:]) 3727 if err != nil { 3728 t.Fatalf("cancel error: %v", err) 3729 } 3730 if tracker.cancel == nil { 3731 t.Fatalf("cancel order not found") 3732 } 3733 3734 ensureErr := func(tag string) { 3735 t.Helper() 3736 err := rig.core.Cancel(oid[:]) 3737 if err == nil { 3738 t.Fatalf("%s: no error", tag) 3739 } 3740 } 3741 3742 // Should get an error for existing cancel order. 3743 ensureErr("second cancel") 3744 3745 // remove the cancel order so we can check its nilness on error. 3746 tracker.cancel = nil 3747 3748 ensureNilCancel := func(tag string) { 3749 if tracker.cancel != nil { 3750 t.Fatalf("%s: cancel order found", tag) 3751 } 3752 } 3753 3754 // Bad order ID 3755 ogID := oid 3756 oid = order.OrderID{0x01, 0x02} 3757 ensureErr("bad id") 3758 ensureNilCancel("bad id") 3759 oid = ogID 3760 3761 // Order not found 3762 delete(dc.trades, oid) 3763 ensureErr("no order") 3764 ensureNilCancel("no order") 3765 dc.trades[oid] = tracker 3766 3767 // Send error 3768 rig.ws.reqErr = tErr 3769 ensureErr("Request error") 3770 ensureNilCancel("Request error") 3771 rig.ws.reqErr = nil 3772 } 3773 3774 func TestHandlePreimageRequest(t *testing.T) { 3775 t.Run("basic checks", func(t *testing.T) { 3776 rig := newTestRig() 3777 defer rig.shutdown() 3778 ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}} 3779 oid := ord.ID() 3780 preImg := newPreimage() 3781 3782 // It is no longer OK for server to omit the commitment. 3783 payload := &msgjson.PreimageRequest{ 3784 OrderID: oid[:], 3785 // No commitment in this request. 3786 } 3787 reqNoCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload) 3788 // mkt := dc.marketConfig(tDcrBtcMktName) 3789 3790 tracker := &trackedTrade{ 3791 Order: ord, 3792 preImg: preImg, 3793 mktID: tDcrBtcMktName, 3794 db: rig.db, 3795 dc: rig.dc, 3796 metaData: &db.OrderMetaData{}, 3797 } 3798 3799 // resetCsum resets csum for further preimage request since multiple 3800 // testing scenarios use the same tracker object. 3801 resetCsum := func(tracker *trackedTrade) { 3802 tracker.csumMtx.Lock() 3803 tracker.csum = nil 3804 tracker.csumMtx.Unlock() 3805 } 3806 3807 rig.dc.trades[oid] = tracker 3808 err := handlePreimageRequest(rig.core, rig.dc, reqNoCommit) 3809 if err == nil { 3810 t.Fatalf("handlePreimageRequest succeeded with no commitment in the request") 3811 } 3812 resetCsum(tracker) 3813 3814 // Test the new path with rig.core.sentCommits. 3815 readyCommitment := func(commit order.Commitment) chan struct{} { 3816 commitSig := make(chan struct{}) // close after fake order submission is "done" 3817 rig.core.sentCommitsMtx.Lock() 3818 rig.core.sentCommits[commit] = commitSig 3819 rig.core.sentCommitsMtx.Unlock() 3820 return commitSig 3821 } 3822 3823 commit := preImg.Commit() 3824 commitSig := readyCommitment(commit) 3825 payload = &msgjson.PreimageRequest{ 3826 OrderID: oid[:], 3827 Commitment: commit[:], 3828 } 3829 reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload) 3830 3831 notes := rig.core.NotificationFeed() 3832 3833 rig.dc.trades[oid] = tracker 3834 err = handlePreimageRequest(rig.core, rig.dc, reqCommit) 3835 if err != nil { 3836 t.Fatalf("handlePreimageRequest error: %v", err) 3837 } 3838 resetCsum(tracker) 3839 3840 // It has gone async now, waiting for commitSig. 3841 // i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..." 3842 close(commitSig) // pretend like the order submission just finished 3843 3844 select { 3845 case note := <-notes.C: 3846 if note.Topic() != TopicPreimageSent { 3847 t.Fatalf("note subject is %v, not %v", note.Topic(), TopicPreimageSent) 3848 } 3849 case <-time.After(time.Second): 3850 t.Fatal("no order note from preimage request handling") 3851 } 3852 3853 // negative paths 3854 ensureErr := func(tag string, req *msgjson.Message, errPrefix string) { 3855 t.Helper() 3856 commitSig := readyCommitment(commit) 3857 close(commitSig) // ready before preimage request 3858 err := handlePreimageRequest(rig.core, rig.dc, req) 3859 if err == nil { 3860 t.Fatalf("%s: no error", tag) 3861 } 3862 if !strings.HasPrefix(err.Error(), errPrefix) { 3863 t.Fatalf("expected error starting with %q, got %q", errPrefix, err) 3864 } 3865 resetCsum(tracker) 3866 } 3867 3868 // unknown commitment in request 3869 payloadBad := &msgjson.PreimageRequest{ 3870 OrderID: oid[:], 3871 Commitment: encode.RandomBytes(order.CommitmentSize), // junk, but correct length 3872 } 3873 reqCommitBad, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payloadBad) 3874 ensureErr("unknown commitment", reqCommitBad, "received preimage request for unknown commitment") 3875 }) 3876 t.Run("csum for order", func(t *testing.T) { 3877 rig := newTestRig() 3878 defer rig.shutdown() 3879 ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}} 3880 oid := ord.ID() 3881 preImg := newPreimage() 3882 // mkt := dc.marketConfig(tDcrBtcMktName) 3883 3884 tracker := &trackedTrade{ 3885 Order: ord, 3886 preImg: preImg, 3887 mktID: tDcrBtcMktName, 3888 db: rig.db, 3889 dc: rig.dc, 3890 metaData: &db.OrderMetaData{}, 3891 } 3892 3893 // Test the new path with rig.core.sentCommits. 3894 readyCommitment := func(commit order.Commitment) chan struct{} { 3895 commitSig := make(chan struct{}) // close after fake order submission is "done" 3896 rig.core.sentCommitsMtx.Lock() 3897 rig.core.sentCommits[commit] = commitSig 3898 rig.core.sentCommitsMtx.Unlock() 3899 return commitSig 3900 } 3901 3902 commit := preImg.Commit() 3903 commitCSum := dex.Bytes{2, 3, 5, 7, 11, 13} 3904 commitSig := readyCommitment(commit) 3905 payload := &msgjson.PreimageRequest{ 3906 OrderID: oid[:], 3907 Commitment: commit[:], 3908 CommitChecksum: commitCSum, 3909 } 3910 reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload) 3911 3912 notes := rig.core.NotificationFeed() 3913 3914 rig.dc.trades[oid] = tracker 3915 err := handlePreimageRequest(rig.core, rig.dc, reqCommit) 3916 if err != nil { 3917 t.Fatalf("handlePreimageRequest error: %v", err) 3918 } 3919 3920 // It has gone async now, waiting for commitSig. 3921 // i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..." 3922 close(commitSig) // pretend like the order submission just finished 3923 3924 select { 3925 case note := <-notes.C: 3926 if note.Topic() != TopicPreimageSent { 3927 t.Fatalf("note subject is %v, not %v", note.Topic(), TopicPreimageSent) 3928 } 3929 case <-time.After(time.Second): 3930 t.Fatal("no order note from preimage request handling") 3931 } 3932 3933 tracker.csumMtx.RLock() 3934 csum := tracker.csum 3935 tracker.csumMtx.RUnlock() 3936 if !bytes.Equal(commitCSum, csum) { 3937 t.Fatalf( 3938 "handlePreimageRequest must initialize tracker csum, exp: %s, got: %s", 3939 commitCSum, 3940 csum, 3941 ) 3942 } 3943 3944 }) 3945 t.Run("more than one preimage request for order (different csums)", func(t *testing.T) { 3946 rig := newTestRig() 3947 defer rig.shutdown() 3948 ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}} 3949 oid := ord.ID() 3950 preImg := newPreimage() 3951 // mkt := dc.marketConfig(tDcrBtcMktName) 3952 firstCSum := dex.Bytes{2, 3, 5, 7, 11, 13} 3953 3954 tracker := &trackedTrade{ 3955 Order: ord, 3956 preImg: preImg, 3957 mktID: tDcrBtcMktName, 3958 db: rig.db, 3959 dc: rig.dc, 3960 // Simulate first preimage request by initializing csum here. 3961 csum: firstCSum, 3962 metaData: &db.OrderMetaData{}, 3963 } 3964 3965 // Test the new path with rig.core.sentCommits. 3966 readyCommitment := func(commit order.Commitment) chan struct{} { 3967 commitSig := make(chan struct{}) // close after fake order submission is "done" 3968 rig.core.sentCommitsMtx.Lock() 3969 rig.core.sentCommits[commit] = commitSig 3970 rig.core.sentCommitsMtx.Unlock() 3971 return commitSig 3972 } 3973 3974 commit := preImg.Commit() 3975 commitSig := readyCommitment(commit) 3976 secondCSum := dex.Bytes{2, 3, 5, 7, 11, 14} 3977 payload := &msgjson.PreimageRequest{ 3978 OrderID: oid[:], 3979 Commitment: commit[:], 3980 CommitChecksum: secondCSum, 3981 } 3982 reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload) 3983 3984 // Prepare to have processPreimageRequest respond with a payload with 3985 // the Error field set. 3986 rig.ws.sendMsgErrChan = make(chan *msgjson.Error, 1) 3987 defer func() { rig.ws.sendMsgErrChan = nil }() 3988 3989 rig.dc.trades[oid] = tracker 3990 err := handlePreimageRequest(rig.core, rig.dc, reqCommit) 3991 if err != nil { 3992 t.Fatalf("handlePreimageRequest error: %v", err) 3993 } 3994 3995 // It has gone async now, waiting for commitSig. 3996 // i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..." 3997 close(commitSig) // pretend like the order submission just finished 3998 3999 select { 4000 case msgErr := <-rig.ws.sendMsgErrChan: 4001 if msgErr.Code != msgjson.InvalidRequestError { 4002 t.Fatalf("expected error code %d got %d", msgjson.InvalidRequestError, msgErr.Code) 4003 } 4004 case <-time.After(time.Second): 4005 t.Fatal("no msgjson.Error sent from preimage request handling") 4006 } 4007 4008 tracker.csumMtx.RLock() 4009 csum := tracker.csum 4010 tracker.csumMtx.RUnlock() 4011 if !bytes.Equal(firstCSum, csum) { 4012 t.Fatalf( 4013 "[handlePreimageRequest] csum was changed, exp: %s, got: %s", 4014 firstCSum, 4015 csum, 4016 ) 4017 } 4018 4019 }) 4020 t.Run("more than one preimage request for order (same csum)", func(t *testing.T) { 4021 rig := newTestRig() 4022 defer rig.shutdown() 4023 ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}} 4024 oid := ord.ID() 4025 preImg := newPreimage() 4026 // mkt := dc.marketConfig(tDcrBtcMktName) 4027 csum := dex.Bytes{2, 3, 5, 7, 11, 13} 4028 4029 tracker := &trackedTrade{ 4030 Order: ord, 4031 preImg: preImg, 4032 mktID: tDcrBtcMktName, 4033 db: rig.db, 4034 dc: rig.dc, 4035 // Simulate first preimage request by initializing csum here. 4036 csum: csum, 4037 metaData: &db.OrderMetaData{}, 4038 } 4039 4040 // Test the new path with rig.core.sentCommits. 4041 readyCommitment := func(commit order.Commitment) chan struct{} { 4042 commitSig := make(chan struct{}) // close after fake order submission is "done" 4043 rig.core.sentCommitsMtx.Lock() 4044 rig.core.sentCommits[commit] = commitSig 4045 rig.core.sentCommitsMtx.Unlock() 4046 return commitSig 4047 } 4048 4049 commit := preImg.Commit() 4050 commitSig := readyCommitment(commit) 4051 payload := &msgjson.PreimageRequest{ 4052 OrderID: oid[:], 4053 Commitment: commit[:], 4054 CommitChecksum: csum, 4055 } 4056 reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload) 4057 4058 notes := rig.core.NotificationFeed() 4059 4060 rig.dc.trades[oid] = tracker 4061 err := handlePreimageRequest(rig.core, rig.dc, reqCommit) 4062 if err != nil { 4063 t.Fatalf("handlePreimageRequest error: %v", err) 4064 } 4065 4066 // It has gone async now, waiting for commitSig. 4067 // i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..." 4068 close(commitSig) // pretend like the order submission just finished 4069 4070 select { 4071 case note := <-notes.C: 4072 if note.Topic() != TopicPreimageSent { 4073 t.Fatalf("note subject is %v, not %v", note.Topic(), TopicPreimageSent) 4074 } 4075 case <-time.After(time.Second): 4076 t.Fatal("no order note from preimage request handling") 4077 } 4078 4079 tracker.csumMtx.RLock() 4080 checkSum := tracker.csum 4081 tracker.csumMtx.RUnlock() 4082 if !bytes.Equal(csum, checkSum) { 4083 t.Fatalf( 4084 "[handlePreimageRequest] csum was changed, exp: %s, got: %s", 4085 csum, 4086 checkSum, 4087 ) 4088 } 4089 }) 4090 t.Run("csum for cancel order", func(t *testing.T) { 4091 rig := newTestRig() 4092 defer rig.shutdown() 4093 ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}} 4094 preImg := newPreimage() 4095 mkt := rig.dc.marketConfig(tDcrBtcMktName) 4096 4097 tracker := &trackedTrade{ 4098 Order: ord, 4099 preImg: preImg, 4100 mktID: tDcrBtcMktName, 4101 db: rig.db, 4102 dc: rig.dc, 4103 metaData: &db.OrderMetaData{}, 4104 cancel: &trackedCancel{ 4105 CancelOrder: order.CancelOrder{ 4106 P: order.Prefix{ 4107 AccountID: rig.dc.acct.ID(), 4108 BaseAsset: tUTXOAssetA.ID, 4109 QuoteAsset: tUTXOAssetB.ID, 4110 OrderType: order.MarketOrderType, 4111 ClientTime: time.Now(), 4112 ServerTime: time.Now().Add(time.Millisecond), 4113 Commit: preImg.Commit(), 4114 }, 4115 }, 4116 epochLen: mkt.EpochLen, 4117 }, 4118 } 4119 oid := tracker.ID() 4120 cid := tracker.cancel.ID() 4121 4122 // Test the new path with rig.core.sentCommits. 4123 readyCommitment := func(commit order.Commitment) chan struct{} { 4124 commitSig := make(chan struct{}) // close after fake order submission is "done" 4125 rig.core.sentCommitsMtx.Lock() 4126 rig.core.sentCommits[commit] = commitSig 4127 rig.core.sentCommitsMtx.Unlock() 4128 return commitSig 4129 } 4130 4131 commit := preImg.Commit() 4132 commitCSum := dex.Bytes{2, 3, 5, 7, 11, 13} 4133 commitSig := readyCommitment(commit) 4134 payload := &msgjson.PreimageRequest{ 4135 OrderID: cid[:], 4136 Commitment: commit[:], 4137 CommitChecksum: commitCSum, 4138 } 4139 reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload) 4140 4141 notes := rig.core.NotificationFeed() 4142 4143 rig.dc.trades[oid] = tracker 4144 rig.dc.registerCancelLink(cid, oid) 4145 err := handlePreimageRequest(rig.core, rig.dc, reqCommit) 4146 if err != nil { 4147 t.Fatalf("handlePreimageRequest error: %v", err) 4148 } 4149 4150 // It has gone async now, waiting for commitSig. 4151 // i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..." 4152 close(commitSig) // pretend like the order submission just finished 4153 4154 select { 4155 case note := <-notes.C: 4156 if note.Topic() != TopicCancelPreimageSent { 4157 t.Fatalf("note subject is %v, not %v", note.Topic(), TopicCancelPreimageSent) 4158 } 4159 case <-time.After(time.Second): 4160 t.Fatal("no order note from preimage request handling") 4161 } 4162 4163 tracker.csumMtx.RLock() 4164 cancelCsum := tracker.cancelCsum 4165 tracker.csumMtx.RUnlock() 4166 if !bytes.Equal(commitCSum, cancelCsum) { 4167 t.Fatalf( 4168 "handlePreimageRequest must initialize tracker cancel csum, exp: %s, got: %s", 4169 commitCSum, 4170 cancelCsum, 4171 ) 4172 } 4173 4174 }) 4175 t.Run("more than one preimage request for cancel order (different csums)", func(t *testing.T) { 4176 rig := newTestRig() 4177 defer rig.shutdown() 4178 ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}} 4179 preImg := newPreimage() 4180 mkt := rig.dc.marketConfig(tDcrBtcMktName) 4181 firstCSum := dex.Bytes{2, 3, 5, 7, 11, 13} 4182 4183 tracker := &trackedTrade{ 4184 Order: ord, 4185 preImg: preImg, 4186 mktID: tDcrBtcMktName, 4187 db: rig.db, 4188 dc: rig.dc, 4189 metaData: &db.OrderMetaData{}, 4190 // Simulate first preimage request by initializing csum here. 4191 cancelCsum: firstCSum, 4192 cancel: &trackedCancel{ 4193 CancelOrder: order.CancelOrder{ 4194 P: order.Prefix{ 4195 AccountID: rig.dc.acct.ID(), 4196 BaseAsset: tUTXOAssetA.ID, 4197 QuoteAsset: tUTXOAssetB.ID, 4198 OrderType: order.MarketOrderType, 4199 ClientTime: time.Now(), 4200 ServerTime: time.Now().Add(time.Millisecond), 4201 Commit: preImg.Commit(), 4202 }, 4203 }, 4204 epochLen: mkt.EpochLen, 4205 }, 4206 } 4207 oid := tracker.ID() 4208 cid := tracker.cancel.ID() 4209 4210 // Test the new path with rig.core.sentCommits. 4211 readyCommitment := func(commit order.Commitment) chan struct{} { 4212 commitSig := make(chan struct{}) // close after fake order submission is "done" 4213 rig.core.sentCommitsMtx.Lock() 4214 rig.core.sentCommits[commit] = commitSig 4215 rig.core.sentCommitsMtx.Unlock() 4216 return commitSig 4217 } 4218 4219 commit := preImg.Commit() 4220 secondCSum := dex.Bytes{2, 3, 5, 7, 11, 14} 4221 commitSig := readyCommitment(commit) 4222 payload := &msgjson.PreimageRequest{ 4223 OrderID: cid[:], 4224 Commitment: commit[:], 4225 CommitChecksum: secondCSum, 4226 } 4227 reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload) 4228 4229 // Prepare to have processPreimageRequest respond with a payload with 4230 // the Error field set. 4231 rig.ws.sendMsgErrChan = make(chan *msgjson.Error, 1) 4232 defer func() { rig.ws.sendMsgErrChan = nil }() 4233 4234 rig.dc.trades[oid] = tracker 4235 rig.dc.registerCancelLink(cid, oid) 4236 err := handlePreimageRequest(rig.core, rig.dc, reqCommit) 4237 if err != nil { 4238 t.Fatalf("handlePreimageRequest error: %v", err) 4239 } 4240 4241 // It has gone async now, waiting for commitSig. 4242 // i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..." 4243 close(commitSig) // pretend like the order submission just finished 4244 4245 select { 4246 case msgErr := <-rig.ws.sendMsgErrChan: 4247 if msgErr.Code != msgjson.InvalidRequestError { 4248 t.Fatalf("expected error code %d got %d", msgjson.InvalidRequestError, msgErr.Code) 4249 } 4250 case <-time.After(time.Second): 4251 t.Fatal("no msgjson.Error sent from preimage request handling") 4252 } 4253 tracker.csumMtx.RLock() 4254 cancelCsum := tracker.cancelCsum 4255 tracker.csumMtx.RUnlock() 4256 if !bytes.Equal(firstCSum, cancelCsum) { 4257 t.Fatalf( 4258 "[handlePreimageRequest] cancel csum was changed, exp: %s, got: %s", 4259 firstCSum, 4260 cancelCsum, 4261 ) 4262 } 4263 }) 4264 t.Run("more than one preimage request for cancel order (same csum)", func(t *testing.T) { 4265 rig := newTestRig() 4266 defer rig.shutdown() 4267 ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}} 4268 preImg := newPreimage() 4269 mkt := rig.dc.marketConfig(tDcrBtcMktName) 4270 csum := dex.Bytes{2, 3, 5, 7, 11, 13} 4271 4272 tracker := &trackedTrade{ 4273 Order: ord, 4274 preImg: preImg, 4275 mktID: tDcrBtcMktName, 4276 db: rig.db, 4277 dc: rig.dc, 4278 metaData: &db.OrderMetaData{}, 4279 // Simulate first preimage request by initializing csum here. 4280 cancelCsum: csum, 4281 cancel: &trackedCancel{ 4282 CancelOrder: order.CancelOrder{ 4283 P: order.Prefix{ 4284 AccountID: rig.dc.acct.ID(), 4285 BaseAsset: tUTXOAssetA.ID, 4286 QuoteAsset: tUTXOAssetB.ID, 4287 OrderType: order.MarketOrderType, 4288 ClientTime: time.Now(), 4289 ServerTime: time.Now().Add(time.Millisecond), 4290 Commit: preImg.Commit(), 4291 }, 4292 }, 4293 epochLen: mkt.EpochLen, 4294 }, 4295 } 4296 oid := tracker.ID() 4297 cid := tracker.cancel.ID() 4298 4299 // Test the new path with rig.core.sentCommits. 4300 readyCommitment := func(commit order.Commitment) chan struct{} { 4301 commitSig := make(chan struct{}) // close after fake order submission is "done" 4302 rig.core.sentCommitsMtx.Lock() 4303 rig.core.sentCommits[commit] = commitSig 4304 rig.core.sentCommitsMtx.Unlock() 4305 return commitSig 4306 } 4307 4308 commit := preImg.Commit() 4309 commitSig := readyCommitment(commit) 4310 payload := &msgjson.PreimageRequest{ 4311 OrderID: cid[:], 4312 Commitment: commit[:], 4313 CommitChecksum: csum, 4314 } 4315 reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload) 4316 4317 notes := rig.core.NotificationFeed() 4318 4319 rig.dc.trades[oid] = tracker 4320 rig.dc.registerCancelLink(cid, oid) 4321 err := handlePreimageRequest(rig.core, rig.dc, reqCommit) 4322 if err != nil { 4323 t.Fatalf("handlePreimageRequest error: %v", err) 4324 } 4325 4326 // It has gone async now, waiting for commitSig. 4327 // i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..." 4328 close(commitSig) // pretend like the order submission just finished 4329 4330 select { 4331 case note := <-notes.C: 4332 if note.Topic() != TopicCancelPreimageSent { 4333 t.Fatalf("note subject is %v, not %v", note.Topic(), TopicCancelPreimageSent) 4334 } 4335 case <-time.After(time.Second): 4336 t.Fatal("no order note from preimage request handling") 4337 } 4338 4339 tracker.csumMtx.RLock() 4340 cancelCsum := tracker.cancelCsum 4341 tracker.csumMtx.RUnlock() 4342 if !bytes.Equal(csum, cancelCsum) { 4343 t.Fatalf( 4344 "[handlePreimageRequest] cancel csum was changed, exp: %s, got: %s", 4345 csum, 4346 cancelCsum, 4347 ) 4348 } 4349 }) 4350 } 4351 4352 func TestHandleRevokeOrderMsg(t *testing.T) { 4353 rig := newTestRig() 4354 defer rig.shutdown() 4355 dc := rig.dc 4356 tCore := rig.core 4357 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 4358 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 4359 dcrWallet.address = "DsVmA7aqqWeKWy461hXjytbZbgCqbB8g2dq" 4360 dcrWallet.Unlock(rig.crypter) 4361 4362 fundCoinDcrID := encode.RandomBytes(36) 4363 fundCoinDcr := &tCoin{id: fundCoinDcrID} 4364 4365 btcWallet, _ := newTWallet(tUTXOAssetB.ID) 4366 tCore.wallets[tUTXOAssetB.ID] = btcWallet 4367 btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini" 4368 btcWallet.Unlock(rig.crypter) 4369 4370 // fundCoinBID := encode.RandomBytes(36) 4371 // fundCoinB := &tCoin{id: fundCoinBID} 4372 4373 qty := 2 * dcrBtcLotSize 4374 rate := dcrBtcRateStep * 10 4375 lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, qty, rate) // sell DCR 4376 lo.Coins = []order.CoinID{fundCoinDcrID} 4377 dbOrder.MetaData.Status = order.OrderStatusBooked 4378 oid := lo.ID() 4379 4380 tDcrWallet.fundingCoins = asset.Coins{fundCoinDcr} 4381 4382 walletSet, _, _, err := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true) 4383 if err != nil { 4384 t.Fatalf("walletSet error: %v", err) 4385 } 4386 4387 // Not in dc.trades yet. 4388 4389 // Send a request for the unknown order. 4390 payload := &msgjson.RevokeOrder{ 4391 OrderID: oid[:], 4392 } 4393 req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.RevokeOrderRoute, payload) 4394 4395 // Ensure revoking a non-existent order generates an error. 4396 err = handleRevokeOrderMsg(rig.core, rig.dc, req) 4397 if err == nil { 4398 t.Fatal("[handleRevokeOrderMsg] expected a non-existent order") 4399 } 4400 4401 // Now store the order in dc.trades, with a linked cancel order. 4402 tracker := newTrackedTrade(dbOrder, preImg, dc, 4403 rig.core.lockTimeTaker, rig.core.lockTimeMaker, 4404 rig.db, rig.queue, walletSet, tDcrWallet.fundingCoins, rig.core.notify, 4405 rig.core.formatDetails) 4406 preImgC := newPreimage() 4407 co := &order.CancelOrder{ 4408 P: order.Prefix{ 4409 ServerTime: time.Now(), 4410 Commit: preImgC.Commit(), 4411 }, 4412 } 4413 tracker.cancel = &trackedCancel{CancelOrder: *co} 4414 coid := co.ID() 4415 rig.dc.trades[oid] = tracker 4416 rig.dc.registerCancelLink(coid, oid) 4417 4418 orderNotes, feedDone := orderNoteFeed(tCore) 4419 defer feedDone() 4420 4421 // Revoke the cancel order, not the targeted order. 4422 payloadC := &msgjson.RevokeOrder{ 4423 OrderID: coid[:], 4424 } 4425 reqC, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.RevokeOrderRoute, payloadC) 4426 err = handleRevokeOrderMsg(rig.core, rig.dc, reqC) 4427 if err != nil { 4428 t.Fatalf("handleRevokeOrderMsg error: %v", err) 4429 } 4430 4431 verifyRevokeNotification(orderNotes, TopicFailedCancel, t) 4432 4433 if tracker.metaData.Status == order.OrderStatusRevoked { 4434 t.Errorf("Incorrectly revoked the targeted order instead of clearing the cancel order!") 4435 } 4436 if tracker.cancel != nil { 4437 t.Fatalf("Did not clear the cancel order") 4438 } 4439 4440 // Now revoke the actual trade order. 4441 err = handleRevokeOrderMsg(rig.core, rig.dc, req) 4442 if err != nil { 4443 t.Fatalf("handleRevokeOrderMsg error: %v", err) 4444 } 4445 4446 verifyRevokeNotification(orderNotes, TopicOrderRevoked, t) 4447 4448 if tracker.metaData.Status != order.OrderStatusRevoked { 4449 t.Errorf("expected order status %v, got %v", order.OrderStatusRevoked, tracker.metaData.Status) 4450 } 4451 } 4452 4453 func TestHandleRevokeMatchMsg(t *testing.T) { 4454 rig := newTestRig() 4455 defer rig.shutdown() 4456 dc := rig.dc 4457 tCore := rig.core 4458 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 4459 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 4460 dcrWallet.address = "DsVmA7aqqWeKWy461hXjytbZbgCqbB8g2dq" 4461 dcrWallet.Unlock(rig.crypter) 4462 4463 fundCoinDcrID := encode.RandomBytes(36) 4464 fundCoinDcr := &tCoin{id: fundCoinDcrID} 4465 4466 btcWallet, _ := newTWallet(tUTXOAssetB.ID) 4467 tCore.wallets[tUTXOAssetB.ID] = btcWallet 4468 btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini" 4469 btcWallet.Unlock(rig.crypter) 4470 4471 // fundCoinBID := encode.RandomBytes(36) 4472 // fundCoinB := &tCoin{id: fundCoinBID} 4473 4474 matchSize := 4 * dcrBtcLotSize 4475 cancelledQty := dcrBtcLotSize 4476 qty := 2*matchSize + cancelledQty 4477 lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, qty, dcrBtcRateStep) 4478 lo.Coins = []order.CoinID{fundCoinDcrID} 4479 dbOrder.MetaData.Status = order.OrderStatusBooked 4480 oid := lo.ID() 4481 4482 tDcrWallet.fundingCoins = asset.Coins{fundCoinDcr} 4483 4484 mid := ordertest.RandomMatchID() 4485 walletSet, _, _, err := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true) 4486 if err != nil { 4487 t.Fatalf("walletSet error: %v", err) 4488 } 4489 4490 tracker := newTrackedTrade(dbOrder, preImg, dc, 4491 rig.core.lockTimeTaker, rig.core.lockTimeMaker, 4492 rig.db, rig.queue, walletSet, tDcrWallet.fundingCoins, rig.core.notify, 4493 rig.core.formatDetails) 4494 4495 match := &matchTracker{ 4496 MetaMatch: db.MetaMatch{ 4497 UserMatch: &order.UserMatch{MatchID: mid}, 4498 MetaData: &db.MatchMetaData{}, 4499 }, 4500 } 4501 tracker.matches[mid] = match 4502 4503 payload := &msgjson.RevokeMatch{ 4504 OrderID: oid[:], 4505 MatchID: mid[:], 4506 } 4507 req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.RevokeMatchRoute, payload) 4508 4509 // Ensure revoking a non-existent order generates an error. 4510 err = handleRevokeMatchMsg(rig.core, rig.dc, req) 4511 if err == nil { 4512 t.Fatal("[handleRevokeMatchMsg] expected a non-existent order") 4513 } 4514 4515 rig.dc.trades[oid] = tracker 4516 4517 // Success 4518 err = handleRevokeMatchMsg(rig.core, rig.dc, req) 4519 if err != nil { 4520 t.Fatalf("handleRevokeMatchMsg error: %v", err) 4521 } 4522 } 4523 4524 func TestTradeTracking(t *testing.T) { 4525 rig := newTestRig() 4526 defer rig.shutdown() 4527 dc := rig.dc 4528 tCore := rig.core 4529 tCore.loggedIn = true 4530 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 4531 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 4532 dcrWallet.address = "DsVmA7aqqWeKWy461hXjytbZbgCqbB8g2dq" 4533 dcrWallet.Unlock(rig.crypter) 4534 4535 btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) 4536 tCore.wallets[tUTXOAssetB.ID] = btcWallet 4537 btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini" 4538 btcWallet.Unlock(rig.crypter) 4539 4540 tBtcWallet.confirmRedemptionErr = errors.New("") 4541 tDcrWallet.confirmRedemptionErr = errors.New("") 4542 4543 matchSize := 4 * dcrBtcLotSize 4544 cancelledQty := dcrBtcLotSize 4545 qty := 2*matchSize + cancelledQty 4546 rate := dcrBtcRateStep * 10 4547 lo, dbOrder, preImgL, addr := makeLimitOrder(dc, true, qty, dcrBtcRateStep) 4548 lo.Force = order.StandingTiF 4549 // fundCoinDcrID := encode.RandomBytes(36) 4550 // lo.Coins = []order.CoinID{fundCoinDcrID} 4551 loid := lo.ID() 4552 4553 //fundCoinDcr := &tCoin{id: fundCoinDcrID} 4554 //tDcrWallet.fundingCoins = asset.Coins{fundCoinDcr} 4555 4556 mid := ordertest.RandomMatchID() 4557 walletSet, _, _, err := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true) 4558 if err != nil { 4559 t.Fatalf("walletSet error: %v", err) 4560 } 4561 mkt := dc.marketConfig(tDcrBtcMktName) 4562 fundCoinDcrID := encode.RandomBytes(36) 4563 fundingCoins := asset.Coins{&tCoin{id: fundCoinDcrID}} 4564 tracker := newTrackedTrade(dbOrder, preImgL, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 4565 rig.db, rig.queue, walletSet, fundingCoins, rig.core.notify, rig.core.formatDetails) 4566 rig.dc.trades[tracker.ID()] = tracker 4567 var match *matchTracker 4568 checkStatus := func(tag string, wantStatus order.MatchStatus) { 4569 t.Helper() 4570 if match.Status != wantStatus { 4571 t.Fatalf("%s: wrong status wanted %v, got %v", tag, 4572 wantStatus, match.Status) 4573 } 4574 } 4575 4576 // create new notification feed to catch swap-related errors from goroutines 4577 notes := tCore.NotificationFeed() 4578 drainNotes := func() { 4579 for { 4580 select { 4581 case <-notes.C: 4582 default: 4583 return 4584 } 4585 } 4586 } 4587 4588 lastSwapErrorNote := func() Notification { 4589 for { 4590 select { 4591 case note := <-notes.C: 4592 if note.Severity() == db.ErrorLevel && (note.Topic() == TopicSwapSendError || 4593 note.Topic() == TopicInitError || note.Topic() == TopicReportRedeemError) { 4594 4595 return note 4596 } 4597 default: 4598 return nil 4599 } 4600 } 4601 } 4602 4603 type swapRelatedAction struct { 4604 name string 4605 fn func() error 4606 expectError bool 4607 expectMatchDBUpdates int 4608 expectSwapErrorNote bool 4609 } 4610 testSwapRelatedAction := func(action swapRelatedAction) { 4611 t.Helper() 4612 drainNotes() // clear previous (swap error) notes before exec'ing swap-related action 4613 if action.expectMatchDBUpdates > 0 { 4614 rig.db.updateMatchChan = make(chan order.MatchStatus, action.expectMatchDBUpdates) 4615 } 4616 // Try the action and confirm the behaviour is as expected. 4617 err := action.fn() 4618 if action.expectError && err == nil { 4619 t.Fatalf("%s: expected error but got nil", action.name) 4620 } else if !action.expectError && err != nil { 4621 t.Fatalf("%s: unexpected error: %v", action.name, err) 4622 } 4623 // Check that we received the expected number of match db updates. 4624 for i := 0; i < action.expectMatchDBUpdates; i++ { 4625 <-rig.db.updateMatchChan 4626 } 4627 rig.db.updateMatchChan = nil 4628 // Check that we received a swap error note (if expected), and that 4629 // no error note was received, if not expected. 4630 time.Sleep(100 * time.Millisecond) // wait briefly as swap error notes may be sent from a goroutine 4631 swapErrNote := lastSwapErrorNote() 4632 if action.expectSwapErrorNote && swapErrNote == nil { 4633 t.Fatalf("%s: expected swap error note but got nil", action.name) 4634 } else if !action.expectSwapErrorNote && swapErrNote != nil { 4635 t.Fatalf("%s: unexpected swap error note: %s", action.name, swapErrNote.Details()) 4636 } 4637 } 4638 4639 // MAKER MATCH 4640 matchTime := time.Now() 4641 msgMatch := &msgjson.Match{ 4642 OrderID: loid[:], 4643 MatchID: mid[:], 4644 Quantity: matchSize, 4645 Rate: rate, 4646 Address: "counterparty-address", 4647 Side: uint8(order.Maker), 4648 ServerTime: uint64(matchTime.UnixMilli()), 4649 } 4650 counterSwapID := encode.RandomBytes(36) 4651 tDcrWallet.swapReceipts = []asset.Receipt{&tReceipt{coin: &tCoin{id: counterSwapID}}} 4652 sign(tDexPriv, msgMatch) 4653 4654 // Make sure that a fee rate higher than our recorded MaxFeeRate results in 4655 // an error. 4656 msgMatch.FeeRateBase = tMaxFeeRate + 1 4657 sign(tDexPriv, msgMatch) 4658 msg, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch}) 4659 err = handleMatchRoute(tCore, rig.dc, msg) 4660 if err == nil || !strings.Contains(err.Error(), "is > MaxFeeRate") { 4661 t.Fatalf("no error for fee rate > MaxFeeRate %t", lo.Trade().Sell) 4662 } 4663 4664 // Restore fee rate. 4665 msgMatch.FeeRateBase = tMaxFeeRate 4666 sign(tDexPriv, msgMatch) 4667 msg, _ = msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch}) 4668 4669 // Handle new match as maker with a queued invalid DEX init ack. 4670 // handleMatchRoute should have no errors but trigger a match db update (status = NewlyMatched). 4671 // Maker's swap should be bcasted, triggering another match db update (NewlyMatched->MakerSwapCast). 4672 // sendInitAsync should fail because of invalid ack and produce a swap error note. 4673 testSwapRelatedAction(swapRelatedAction{ 4674 name: "handleMatchRoute", 4675 fn: func() error { 4676 // queue an invalid DEX init ack 4677 rig.ws.queueResponse(msgjson.InitRoute, invalidAcker) 4678 return handleMatchRoute(tCore, rig.dc, msg) 4679 }, 4680 expectError: false, 4681 expectMatchDBUpdates: 2, 4682 expectSwapErrorNote: true, 4683 }) 4684 4685 var found bool 4686 match, found = tracker.matches[mid] 4687 if !found { 4688 t.Fatalf("match not found") 4689 } 4690 4691 // We're the maker, so the init transaction should be broadcast. 4692 checkStatus("maker swapped", order.MakerSwapCast) 4693 proof, auth := &match.MetaData.Proof, &match.MetaData.Proof.Auth 4694 if len(auth.MatchSig) == 0 { 4695 t.Fatalf("no match sig recorded") 4696 } 4697 if !bytes.Equal(proof.MakerSwap, counterSwapID) { 4698 t.Fatalf("receipt ID not recorded") 4699 } 4700 if len(proof.Secret) == 0 { 4701 t.Fatalf("secret not set") 4702 } 4703 if len(proof.SecretHash) == 0 { 4704 t.Fatalf("secret hash not set") 4705 } 4706 // auth.InitSig should be unset because our init request received 4707 // an invalid ack 4708 if len(auth.InitSig) != 0 { 4709 t.Fatalf("init sig recorded for invalid init ack") 4710 } 4711 4712 // requeue an invalid DEX init ack and resend pending init request 4713 testSwapRelatedAction(swapRelatedAction{ 4714 name: "resend pending init (invalid ack)", 4715 fn: func() error { 4716 rig.ws.queueResponse(msgjson.InitRoute, invalidAcker) 4717 tCore.resendPendingRequests(tracker) 4718 return nil 4719 }, 4720 expectError: false, 4721 expectMatchDBUpdates: 0, // no db update for invalid init ack 4722 expectSwapErrorNote: true, // expect swap error note for invalid init ack 4723 }) 4724 // auth.InitSig should remain unset because our resent init request 4725 // received an invalid ack still 4726 if len(auth.InitSig) != 0 { 4727 t.Fatalf("init sig recorded for second invalid init ack") 4728 } 4729 4730 // queue a valid DEX init ack and re-send pending init request 4731 // a valid ack should produce a db update otherwise it's an error 4732 testSwapRelatedAction(swapRelatedAction{ 4733 name: "resend pending init (valid ack)", 4734 fn: func() error { 4735 rig.ws.queueResponse(msgjson.InitRoute, initAcker) 4736 tCore.resendPendingRequests(tracker) 4737 return nil 4738 }, 4739 expectError: false, 4740 expectMatchDBUpdates: 1, // expect db update for valid init ack 4741 expectSwapErrorNote: false, // no swap error note for valid init ack 4742 }) 4743 // auth.InitSig should now be set because our init request received 4744 // a valid ack 4745 if len(auth.InitSig) == 0 { 4746 t.Fatalf("init sig not recorded for valid init ack") 4747 } 4748 4749 // Send the counter-party's init info. 4750 auditQty := calc.BaseToQuote(rate, matchSize) 4751 audit, auditInfo := tMsgAudit(loid, mid, addr, auditQty, proof.SecretHash) 4752 auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeTaker)) 4753 tBtcWallet.auditInfo = auditInfo 4754 msg, _ = msgjson.NewRequest(1, msgjson.AuditRoute, audit) 4755 4756 // Check audit errors. 4757 tBtcWallet.auditErr = tErr 4758 err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil) 4759 if err == nil { 4760 t.Fatalf("no maker error for AuditContract error") 4761 } 4762 4763 // Check expiration error. 4764 match.MetaData.Proof.SelfRevoked = true // keeps trying unless revoked 4765 tBtcWallet.auditErr = asset.CoinNotFoundError 4766 err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil) 4767 if err == nil { 4768 t.Fatalf("no maker error for AuditContract expiration") 4769 } 4770 var expErr ExpirationErr 4771 if !errors.As(err, &expErr) { 4772 t.Fatalf("wrong error type. expecting ExpirationTimeout, got %T: %v", err, err) 4773 } 4774 tBtcWallet.auditErr = nil 4775 match.MetaData.Proof.SelfRevoked = false 4776 4777 auditInfo.Coin.(*tCoin).val = auditQty - 1 4778 err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil) 4779 if err == nil { 4780 t.Fatalf("no maker error for low value") 4781 } 4782 auditInfo.Coin.(*tCoin).val = auditQty 4783 4784 auditInfo.SecretHash = []byte{0x01} 4785 err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil) 4786 if err == nil { 4787 t.Fatalf("no maker error for wrong secret hash") 4788 } 4789 auditInfo.SecretHash = proof.SecretHash 4790 4791 auditInfo.Recipient = "wrong address" 4792 err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil) 4793 if err == nil { 4794 t.Fatalf("no maker error for wrong address") 4795 } 4796 auditInfo.Recipient = addr 4797 4798 auditInfo.Expiration = matchTime.Add(tracker.lockTimeTaker - time.Hour) 4799 err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil) 4800 if err == nil { 4801 t.Fatalf("no maker error for early lock time") 4802 } 4803 auditInfo.Expiration = matchTime.Add(tracker.lockTimeTaker) 4804 4805 // success, full handleAuditRoute>processAuditMsg>auditContract 4806 rig.db.updateMatchChan = make(chan order.MatchStatus, 1) 4807 err = handleAuditRoute(tCore, rig.dc, msg) 4808 if err != nil { 4809 t.Fatalf("audit error: %v", err) 4810 } 4811 // let the async auditContract run 4812 newMatchStatus := <-rig.db.updateMatchChan 4813 if newMatchStatus != order.TakerSwapCast { 4814 t.Fatalf("wrong match status. wanted %v, got %v", order.TakerSwapCast, newMatchStatus) 4815 } 4816 if match.counterSwap == nil { 4817 t.Fatalf("counter-swap not set") 4818 } 4819 if !bytes.Equal(proof.CounterContract, audit.Contract) { 4820 t.Fatalf("counter-script not recorded") 4821 } 4822 if !bytes.Equal(proof.TakerSwap, audit.CoinID) { 4823 t.Fatalf("taker contract ID not set") 4824 } 4825 <-rig.db.updateMatchChan // AuditSig is set in a second match data update 4826 if !bytes.Equal(auth.AuditSig, audit.Sig) { 4827 t.Fatalf("audit sig not set") 4828 } 4829 if auth.AuditStamp != audit.Time { 4830 t.Fatalf("audit time not set") 4831 } 4832 4833 // Confirming the counter-swap triggers a redemption. 4834 tBtcWallet.setConfs(auditInfo.Coin.ID(), tUTXOAssetB.SwapConf, nil) 4835 redeemCoin := encode.RandomBytes(36) 4836 //<-tBtcWallet.redeemErrChan 4837 tBtcWallet.redeemCoins = []dex.Bytes{redeemCoin} 4838 rig.ws.queueResponse(msgjson.RedeemRoute, redeemAcker) 4839 tCore.tickAsset(dc, tUTXOAssetB.ID) 4840 // TakerSwapCast -> MakerRedeemed after broadcast, before redeem request 4841 newMatchStatus = <-rig.db.updateMatchChan 4842 if newMatchStatus != order.MakerRedeemed { 4843 t.Fatalf("wrong match status. wanted %v, got %v", order.MakerRedeemed, newMatchStatus) 4844 } 4845 // MakerRedeem -> MatchComplete after redeem request 4846 newMatchStatus = <-rig.db.updateMatchChan 4847 if newMatchStatus != order.MatchComplete { 4848 t.Fatalf("wrong match status. wanted %v, got %v", order.MatchComplete, newMatchStatus) 4849 } 4850 if !bytes.Equal(proof.MakerRedeem, redeemCoin) { 4851 t.Fatalf("redeem coin ID not logged") 4852 } 4853 // No redemption request received as maker. Only taker gets a redemption 4854 // request following maker's redeem. 4855 4856 // Check that fees were incremented appropriately. 4857 if tracker.metaData.SwapFeesPaid != tSwapFeesPaid { 4858 t.Fatalf("wrong fees recorded for swap. expected %d, got %d", tSwapFeesPaid, tracker.metaData.SwapFeesPaid) 4859 } 4860 // Check that fees were incremented appropriately. 4861 if tracker.metaData.RedemptionFeesPaid != tRedemptionFeesPaid { 4862 t.Fatalf("wrong fees recorded for redemption. expected %d, got %d", tRedemptionFeesPaid, tracker.metaData.SwapFeesPaid) 4863 } 4864 rig.db.updateMatchChan = nil 4865 4866 // TAKER MATCH 4867 // 4868 mid = ordertest.RandomMatchID() 4869 msgMatch = &msgjson.Match{ 4870 OrderID: loid[:], 4871 MatchID: mid[:], 4872 Quantity: matchSize, 4873 Rate: rate, 4874 Address: "counterparty-address", 4875 Side: uint8(order.Taker), 4876 ServerTime: uint64(matchTime.UnixMilli()), 4877 FeeRateBase: tMaxFeeRate, 4878 } 4879 sign(tDexPriv, msgMatch) 4880 msg, _ = msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch}) 4881 rig.db.updateMatchChan = make(chan order.MatchStatus, 1) 4882 err = handleMatchRoute(tCore, rig.dc, msg) 4883 if err != nil { 4884 t.Fatalf("match messages error: %v", err) 4885 } 4886 match, found = tracker.matches[mid] 4887 if !found { 4888 t.Fatalf("match not found") 4889 } 4890 newMatchStatus = <-rig.db.updateMatchChan 4891 if newMatchStatus != order.NewlyMatched { 4892 t.Fatalf("wrong match status. wanted %v, got %v", order.NewlyMatched, newMatchStatus) 4893 } 4894 proof, auth = &match.MetaData.Proof, &match.MetaData.Proof.Auth 4895 if len(auth.MatchSig) == 0 { 4896 t.Fatalf("no match sig recorded") 4897 } 4898 // Secret should not be set yet. 4899 if len(proof.Secret) != 0 { 4900 t.Fatalf("secret set for taker") 4901 } 4902 if len(proof.SecretHash) != 0 { 4903 t.Fatalf("secret hash set for taker") 4904 } 4905 4906 // Now send through the audit request for the maker's init. 4907 audit, auditInfo = tMsgAudit(loid, mid, addr, matchSize, nil) 4908 tBtcWallet.auditInfo = auditInfo 4909 // early lock time 4910 auditInfo.Expiration = matchTime.Add(tracker.lockTimeMaker - time.Hour) 4911 err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil) 4912 if err == nil { 4913 t.Fatalf("no taker error for early lock time") 4914 } 4915 4916 // success, full handleAuditRoute>processAuditMsg>auditContract 4917 auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker)) 4918 msg, _ = msgjson.NewRequest(1, msgjson.AuditRoute, audit) 4919 err = handleAuditRoute(tCore, rig.dc, msg) 4920 if err != nil { 4921 t.Fatalf("taker's match message error: %v", err) 4922 } 4923 // let the async auditContract run, updating match status 4924 newMatchStatus = <-rig.db.updateMatchChan 4925 if newMatchStatus != order.MakerSwapCast { 4926 t.Fatalf("wrong match status. wanted %v, got %v", order.MakerSwapCast, newMatchStatus) 4927 } 4928 if len(proof.SecretHash) == 0 { 4929 t.Fatalf("secret hash not set for taker") 4930 } 4931 if !bytes.Equal(proof.MakerSwap, audit.CoinID) { 4932 t.Fatalf("maker redeem coin not set") 4933 } 4934 <-rig.db.updateMatchChan // AuditSig is set in a second match data update 4935 if !bytes.Equal(auth.AuditSig, audit.Sig) { 4936 t.Fatalf("audit sig not set for taker") 4937 } 4938 if auth.AuditStamp != audit.Time { 4939 t.Fatalf("audit time not set for taker") 4940 } 4941 // The swap should not be sent, since the auditInfo coin doesn't have the 4942 // requisite confirmations. 4943 if len(proof.TakerSwap) != 0 { 4944 t.Fatalf("swap broadcast before confirmations") 4945 } 4946 // confirming maker's swap should trigger taker's swap bcast 4947 tBtcWallet.setConfs(auditInfo.Coin.ID(), tUTXOAssetB.SwapConf, nil) 4948 swapID := encode.RandomBytes(36) 4949 tDcrWallet.swapReceipts = []asset.Receipt{&tReceipt{coin: &tCoin{id: swapID}}} 4950 rig.ws.queueResponse(msgjson.InitRoute, initAcker) 4951 tCore.tickAsset(dc, tUTXOAssetB.ID) 4952 newMatchStatus = <-rig.db.updateMatchChan // MakerSwapCast->TakerSwapCast (after taker's swap bcast) 4953 if newMatchStatus != order.TakerSwapCast { 4954 t.Fatalf("wrong match status. wanted %v, got %v", order.TakerSwapCast, newMatchStatus) 4955 } 4956 if len(proof.TakerSwap) == 0 { 4957 t.Fatalf("swap not broadcast with confirmations") 4958 } 4959 <-rig.db.updateMatchChan // init ack sig is set in a second match data update 4960 if len(auth.InitSig) == 0 { 4961 t.Fatalf("init ack sig not set for taker") 4962 } 4963 4964 // Receive the maker's redemption. 4965 redemptionCoin := encode.RandomBytes(36) 4966 redemption := &msgjson.Redemption{ 4967 Redeem: msgjson.Redeem{ 4968 OrderID: loid[:], 4969 MatchID: mid[:], 4970 CoinID: redemptionCoin, 4971 }, 4972 } 4973 sign(tDexPriv, redemption) 4974 redeemCoin = encode.RandomBytes(36) 4975 tBtcWallet.redeemCoins = []dex.Bytes{redeemCoin} 4976 msg, _ = msgjson.NewRequest(1, msgjson.RedemptionRoute, redemption) 4977 4978 tBtcWallet.badSecret = true 4979 err = handleRedemptionRoute(tCore, rig.dc, msg) 4980 if err == nil { 4981 t.Fatalf("no error for wrong secret") 4982 } 4983 newMatchStatus = <-rig.db.updateMatchChan // wrong secret still updates match 4984 if newMatchStatus != order.TakerSwapCast { // but status is same 4985 t.Fatalf("wrong match status. wanted %v, got %v", order.TakerSwapCast, newMatchStatus) 4986 } 4987 tBtcWallet.badSecret = false 4988 4989 tBtcWallet.redeemErrChan = make(chan error, 1) 4990 rig.ws.queueResponse(msgjson.RedeemRoute, redeemAcker) 4991 err = handleRedemptionRoute(tCore, rig.dc, msg) 4992 if err != nil { 4993 t.Fatalf("redemption message error: %v", err) 4994 } 4995 err = <-tBtcWallet.redeemErrChan 4996 if err != nil { 4997 t.Fatalf("should have worked, got: %v", err) 4998 } 4999 // For taker, there's one status update to MakerRedeemed prior to bcasting taker's redemption 5000 newMatchStatus = <-rig.db.updateMatchChan 5001 if newMatchStatus != order.MakerRedeemed { 5002 t.Fatalf("wrong match status. wanted %v, got %v", order.MakerRedeemed, newMatchStatus) 5003 } 5004 // and another status update to MatchComplete when taker's redemption is bcast 5005 newMatchStatus = <-rig.db.updateMatchChan 5006 if newMatchStatus != order.MatchComplete { 5007 t.Fatalf("wrong match status. wanted %v, got %v", order.MatchComplete, newMatchStatus) 5008 } 5009 if !bytes.Equal(proof.MakerRedeem, redemptionCoin) { 5010 t.Fatalf("redemption coin ID not logged") 5011 } 5012 if len(proof.TakerRedeem) == 0 { 5013 t.Fatalf("taker redemption not sent") 5014 } 5015 // Then a match update to set the redeem ack sig when the 'redeem' request back 5016 // to the server succeeds. 5017 <-rig.db.updateMatchChan 5018 if len(auth.RedeemSig) == 0 { 5019 t.Fatalf("redeem ack sig not set for taker") 5020 } 5021 rig.db.updateMatchChan = nil 5022 tBtcWallet.redeemErrChan = nil 5023 5024 // CANCEL ORDER MATCH 5025 // 5026 tDcrWallet.returnedCoins = nil 5027 copy(mid[:], encode.RandomBytes(32)) 5028 preImgC := newPreimage() 5029 co := &order.CancelOrder{ 5030 P: order.Prefix{ 5031 AccountID: dc.acct.ID(), 5032 BaseAsset: tUTXOAssetA.ID, 5033 QuoteAsset: tUTXOAssetB.ID, 5034 OrderType: order.MarketOrderType, 5035 ClientTime: time.Now(), 5036 ServerTime: time.Now().Add(time.Millisecond), 5037 Commit: preImgC.Commit(), 5038 }, 5039 } 5040 tracker.cancel = &trackedCancel{CancelOrder: *co, epochLen: mkt.EpochLen} 5041 coid := co.ID() 5042 rig.dc.registerCancelLink(coid, tracker.ID()) 5043 m1 := &msgjson.Match{ 5044 OrderID: loid[:], 5045 MatchID: mid[:], 5046 Quantity: cancelledQty, 5047 Rate: rate, 5048 Address: "", 5049 } 5050 m2 := &msgjson.Match{ 5051 OrderID: coid[:], 5052 MatchID: mid[:], 5053 Quantity: cancelledQty, 5054 Address: "testaddr", 5055 } 5056 sign(tDexPriv, m1) 5057 sign(tDexPriv, m2) 5058 msg, _ = msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{m1, m2}) 5059 err = handleMatchRoute(tCore, rig.dc, msg) 5060 if err != nil { 5061 t.Fatalf("handleMatchRoute error (cancel with swaps): %v", err) 5062 } 5063 if tracker.cancel.matches.maker == nil { 5064 t.Fatalf("cancelMatches.maker not set") 5065 } 5066 if tracker.Trade().Filled() != qty { 5067 t.Fatalf("fill not set. %d != %d", tracker.Trade().Filled(), qty) 5068 } 5069 if tracker.cancel.matches.taker == nil { 5070 t.Fatalf("cancelMatches.taker not set") 5071 } 5072 // Since there are no unswapped orders, the change coin should be returned. 5073 if len(tDcrWallet.returnedCoins) != 1 || !bytes.Equal(tDcrWallet.returnedCoins[0].ID(), tDcrWallet.changeCoin.id) { 5074 t.Fatalf("change coin not returned") 5075 } 5076 5077 resetMatches := func() { 5078 tracker.matches = make(map[order.MatchID]*matchTracker) 5079 tracker.change = nil 5080 tracker.metaData.ChangeCoin = nil 5081 tracker.coinsLocked = true 5082 } 5083 5084 // If there is no change coin and no matches, the funding coin should be 5085 // returned instead. 5086 resetMatches() 5087 // The change coins would also have been added to the coins map, so delete 5088 // that too. 5089 delete(tracker.coins, tDcrWallet.changeCoin.String()) 5090 err = handleMatchRoute(tCore, rig.dc, msg) 5091 if err != nil { 5092 t.Fatalf("handleMatchRoute error (cancel without swaps): %v", err) 5093 } 5094 if len(tDcrWallet.returnedCoins) != 1 || !bytes.Equal(tDcrWallet.returnedCoins[0].ID(), fundCoinDcrID) { 5095 t.Fatalf("change coin not returned (cancel without swaps)") 5096 } 5097 5098 // If the order is an immediate order, the asset.Swaps.LockChange should be 5099 // false regardless of whether the order is filled. 5100 resetMatches() 5101 tracker.cancel = nil 5102 rig.ws.queueResponse(msgjson.InitRoute, initAcker) 5103 msgMatch.Side = uint8(order.Maker) 5104 sign(tDexPriv, msgMatch) // Side is not in the serialization but whatever 5105 msg, _ = msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch}) 5106 5107 tracker.metaData.Status = order.OrderStatusEpoch 5108 lo.Force = order.ImmediateTiF 5109 err = handleMatchRoute(tCore, rig.dc, msg) 5110 if err != nil { 5111 t.Fatalf("handleMatchRoute error (immediate partial fill): %v", err) 5112 } 5113 lastSwaps := tDcrWallet.lastSwaps[len(tDcrWallet.lastSwaps)-1] 5114 if lastSwaps.LockChange != false { 5115 t.Fatalf("change locked for executed non-standing order (immediate partial fill)") 5116 } 5117 } 5118 5119 func TestReconcileTrades(t *testing.T) { 5120 rig := newTestRig() 5121 defer rig.shutdown() 5122 dc := rig.dc 5123 5124 mkt := dc.marketConfig(tDcrBtcMktName) 5125 rig.core.wallets[mkt.Base], _ = newTWallet(mkt.Base) 5126 rig.core.wallets[mkt.Quote], _ = newTWallet(mkt.Quote) 5127 walletSet, _, _, err := rig.core.walletSet(dc, mkt.Base, mkt.Quote, true) 5128 if err != nil { 5129 t.Fatalf("walletSet error: %v", err) 5130 } 5131 5132 type orderSet struct { 5133 epoch *trackedTrade 5134 booked *trackedTrade // standing limit orders only 5135 bookedPendingCancel *trackedTrade // standing limit orders only 5136 executed *trackedTrade 5137 } 5138 makeOrderSet := func(force order.TimeInForce) *orderSet { 5139 orders := &orderSet{ 5140 epoch: makeTradeTracker(rig, walletSet, force, order.OrderStatusEpoch), 5141 executed: makeTradeTracker(rig, walletSet, force, order.OrderStatusExecuted), 5142 } 5143 if force == order.StandingTiF { 5144 orders.booked = makeTradeTracker(rig, walletSet, force, order.OrderStatusBooked) 5145 orders.bookedPendingCancel = makeTradeTracker(rig, walletSet, force, order.OrderStatusBooked) 5146 orders.bookedPendingCancel.cancel = &trackedCancel{ 5147 CancelOrder: order.CancelOrder{ 5148 P: order.Prefix{ 5149 ServerTime: time.Now().UTC().Add(-16 * time.Minute), 5150 }, 5151 }, 5152 epochLen: mkt.EpochLen, 5153 } 5154 } 5155 return orders 5156 } 5157 5158 standingOrders := makeOrderSet(order.StandingTiF) 5159 immediateOrders := makeOrderSet(order.ImmediateTiF) 5160 5161 tests := []struct { 5162 name string 5163 clientOrders []*trackedTrade // orders known to the client 5164 serverOrders []*msgjson.OrderStatus // orders considered active by the server 5165 orderStatusRes []*msgjson.OrderStatus // server's response to order_status requests 5166 expectOrderStatuses map[order.OrderID]order.OrderStatus 5167 }{ 5168 { 5169 name: "server orders unknown to client", 5170 clientOrders: []*trackedTrade{}, 5171 serverOrders: []*msgjson.OrderStatus{ 5172 { 5173 ID: ordertest.RandomOrderID().Bytes(), 5174 Status: uint16(order.OrderStatusBooked), 5175 }, 5176 }, 5177 expectOrderStatuses: map[order.OrderID]order.OrderStatus{}, 5178 }, 5179 { 5180 name: "different server-client statuses", 5181 clientOrders: []*trackedTrade{ 5182 standingOrders.epoch, 5183 standingOrders.booked, 5184 standingOrders.bookedPendingCancel, 5185 immediateOrders.epoch, 5186 immediateOrders.executed, 5187 }, 5188 serverOrders: []*msgjson.OrderStatus{ 5189 { 5190 ID: standingOrders.epoch.ID().Bytes(), 5191 Status: uint16(order.OrderStatusBooked), // now booked 5192 }, 5193 { 5194 ID: standingOrders.booked.ID().Bytes(), 5195 Status: uint16(order.OrderStatusEpoch), // invald! booked orders cannot return to epoch! 5196 }, 5197 { 5198 ID: standingOrders.bookedPendingCancel.ID().Bytes(), 5199 Status: uint16(order.OrderStatusBooked), // still booked, cancel order should be deleted 5200 }, 5201 { 5202 ID: immediateOrders.epoch.ID().Bytes(), 5203 Status: uint16(order.OrderStatusBooked), // invalid, immediate orders cannot be booked! 5204 }, 5205 { 5206 ID: immediateOrders.executed.ID().Bytes(), 5207 Status: uint16(order.OrderStatusBooked), // invalid, inactive orders should not be returned by DEX! 5208 }, 5209 }, 5210 expectOrderStatuses: map[order.OrderID]order.OrderStatus{ 5211 standingOrders.epoch.ID(): order.OrderStatusBooked, // epoch => booked 5212 standingOrders.booked.ID(): order.OrderStatusBooked, // should not change, cannot return to epoch 5213 standingOrders.bookedPendingCancel.ID(): order.OrderStatusBooked, // no status change 5214 immediateOrders.epoch.ID(): order.OrderStatusEpoch, // should not change, cannot be booked 5215 immediateOrders.executed.ID(): order.OrderStatusExecuted, // should not change, inactive cannot become active 5216 }, 5217 }, 5218 { 5219 name: "active becomes inactive", 5220 clientOrders: []*trackedTrade{ 5221 standingOrders.epoch, 5222 standingOrders.booked, 5223 standingOrders.bookedPendingCancel, 5224 standingOrders.executed, 5225 immediateOrders.epoch, 5226 immediateOrders.executed, 5227 }, 5228 serverOrders: []*msgjson.OrderStatus{}, // no active order reported by server 5229 orderStatusRes: []*msgjson.OrderStatus{ 5230 { 5231 ID: standingOrders.epoch.ID().Bytes(), 5232 Status: uint16(order.OrderStatusRevoked), 5233 }, 5234 { 5235 ID: standingOrders.booked.ID().Bytes(), 5236 Status: uint16(order.OrderStatusRevoked), 5237 }, 5238 { 5239 ID: standingOrders.bookedPendingCancel.ID().Bytes(), 5240 Status: uint16(order.OrderStatusCanceled), 5241 }, 5242 { 5243 ID: immediateOrders.epoch.ID().Bytes(), 5244 Status: uint16(order.OrderStatusExecuted), 5245 }, 5246 }, 5247 expectOrderStatuses: map[order.OrderID]order.OrderStatus{ 5248 standingOrders.epoch.ID(): order.OrderStatusRevoked, // preimage missed = revoked 5249 standingOrders.booked.ID(): order.OrderStatusRevoked, // booked, not canceled = assume revoked (may actually be executed) 5250 standingOrders.bookedPendingCancel.ID(): order.OrderStatusCanceled, // booked pending canceled = assume canceled (may actually be revoked or executed) 5251 standingOrders.executed.ID(): order.OrderStatusExecuted, // should not change 5252 immediateOrders.epoch.ID(): order.OrderStatusExecuted, // preimage sent, not canceled = executed 5253 immediateOrders.executed.ID(): order.OrderStatusExecuted, // should not change 5254 }, 5255 }, 5256 } 5257 5258 for _, tt := range tests { 5259 // Track client orders in dc.trades. 5260 dc.tradeMtx.Lock() 5261 var pendingCancel *trackedTrade 5262 dc.trades = make(map[order.OrderID]*trackedTrade) 5263 for _, tracker := range tt.clientOrders { 5264 dc.trades[tracker.ID()] = tracker 5265 if tracker.cancel != nil { 5266 pendingCancel = tracker 5267 } 5268 } 5269 dc.tradeMtx.Unlock() 5270 5271 // Queue order_status response if required for reconciliation. 5272 if len(tt.orderStatusRes) > 0 { 5273 rig.ws.queueResponse(msgjson.OrderStatusRoute, func(msg *msgjson.Message, f msgFunc) error { 5274 resp, _ := msgjson.NewResponse(msg.ID, tt.orderStatusRes, nil) 5275 f(resp) 5276 return nil 5277 }) 5278 } 5279 5280 // Reconcile tracked orders with server orders. 5281 dc.reconcileTrades(tt.serverOrders) 5282 5283 dc.tradeMtx.RLock() 5284 if len(dc.trades) != len(tt.expectOrderStatuses) { 5285 t.Fatalf("%s: post-reconcileTrades order count mismatch. expected %d, got %d", 5286 tt.name, len(tt.expectOrderStatuses), len(dc.trades)) 5287 } 5288 for oid, tracker := range dc.trades { 5289 expectedStatus, expected := tt.expectOrderStatuses[oid] 5290 if !expected { 5291 t.Fatalf("%s: unexpected order %v tracked by client", tt.name, oid) 5292 } 5293 tracker.mtx.RLock() 5294 if tracker.metaData.Status != expectedStatus { 5295 t.Fatalf("%s: client reported wrong order status %v, expected %v", 5296 tt.name, tracker.metaData.Status, expectedStatus) 5297 } 5298 tracker.mtx.RUnlock() 5299 } 5300 dc.tradeMtx.RUnlock() 5301 5302 // Check if a previously canceled order existed; if the order is still 5303 // active (Epoch/Booked status) and the cancel order is deleted, having 5304 // been there for over 15 minutes since the cancel order's epoch ended. 5305 if pendingCancel != nil { 5306 pendingCancel.mtx.RLock() 5307 status, stillHasCancelOrder := pendingCancel.metaData.Status, pendingCancel.cancel != nil 5308 pendingCancel.mtx.RUnlock() 5309 if status == order.OrderStatusBooked { 5310 if stillHasCancelOrder { 5311 t.Fatalf("%s: expected stale cancel order to be deleted for now-booked order", tt.name) 5312 } 5313 // Cancel order deleted. Canceling the order again should succeed. 5314 rig.queueCancel(nil) 5315 err = rig.core.Cancel(pendingCancel.ID().Bytes()) 5316 if err != nil { 5317 t.Fatalf("cancel order error after deleting previous stale cancel: %v", err) 5318 } 5319 } 5320 } 5321 } 5322 } 5323 5324 func makeTradeTracker(rig *testRig, walletSet *walletSet, force order.TimeInForce, status order.OrderStatus) *trackedTrade { 5325 qty := 4 * dcrBtcLotSize 5326 lo, dbOrder, preImg, _ := makeLimitOrder(rig.dc, true, qty, dcrBtcRateStep) 5327 lo.Force = force 5328 dbOrder.MetaData.Status = status 5329 5330 return newTrackedTrade(dbOrder, preImg, rig.dc, 5331 rig.core.lockTimeTaker, rig.core.lockTimeMaker, 5332 rig.db, rig.queue, walletSet, nil, rig.core.notify, 5333 rig.core.formatDetails) 5334 } 5335 5336 func TestRefunds(t *testing.T) { 5337 rig := newTestRig() 5338 defer rig.shutdown() 5339 5340 dc := rig.dc 5341 tCore := rig.core 5342 btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) 5343 tCore.wallets[tUTXOAssetB.ID] = btcWallet 5344 btcWallet.address = "DsVmA7aqqWeKWy461hXjytbZbgCqbB8g2dq" 5345 btcWallet.Unlock(rig.crypter) 5346 5347 ethWallet, tEthWallet := newTAccountLocker(tACCTAsset.ID) 5348 tCore.wallets[tACCTAsset.ID] = ethWallet 5349 ethWallet.address = "18d65fb8d60c1199bb1ad381be47aa692b482605" 5350 ethWallet.Unlock(rig.crypter) 5351 5352 checkStatus := func(tag string, match *matchTracker, wantStatus order.MatchStatus) { 5353 t.Helper() 5354 if match.Status != wantStatus { 5355 t.Fatalf("%s: wrong status wanted %v, got %v", tag, wantStatus, match.Status) 5356 } 5357 } 5358 checkRefund := func(tracker *trackedTrade, match *matchTracker, expectAmt uint64) { 5359 t.Helper() 5360 // Confirm that the status is SwapCast. 5361 if match.Side == order.Maker { 5362 checkStatus("maker swapped", match, order.MakerSwapCast) 5363 } else { 5364 checkStatus("taker swapped", match, order.TakerSwapCast) 5365 } 5366 // Confirm isRefundable = true. 5367 if !tracker.isRefundable(tCore.ctx, match) { 5368 t.Fatalf("%s's swap not refundable", match.Side) 5369 } 5370 // Check refund. 5371 amtRefunded, err := rig.core.refundMatches(tracker, []*matchTracker{match}) 5372 if err != nil { 5373 t.Fatalf("unexpected refund error %v", err) 5374 } 5375 // Check refunded amount. 5376 if amtRefunded != expectAmt { 5377 t.Fatalf("expected %d refund amount, got %d", expectAmt, amtRefunded) 5378 } 5379 // Confirm isRefundable = false. 5380 if tracker.isRefundable(tCore.ctx, match) { 5381 t.Fatalf("%s's swap refundable after being refunded", match.Side) 5382 } 5383 // Expect refund re-attempt to not refund any coin. 5384 amtRefunded, err = rig.core.refundMatches(tracker, []*matchTracker{match}) 5385 if err != nil { 5386 t.Fatalf("unexpected refund error %v", err) 5387 } 5388 if amtRefunded != 0 { 5389 t.Fatalf("expected 0 refund amount, got %d", amtRefunded) 5390 } 5391 // Confirm that the status is unchanged. 5392 if match.Side == order.Maker { 5393 checkStatus("maker swapped", match, order.MakerSwapCast) 5394 } else { 5395 checkStatus("taker swapped", match, order.TakerSwapCast) 5396 } 5397 5398 if _, is := tracker.accountRefunder(); is { 5399 if tEthWallet.refundFeeSuggestion != tMaxFeeRate { 5400 t.Fatalf("refund suggestion for account asset %v != server max fee rate %v", 5401 tEthWallet.refundFeeSuggestion, tACCTAsset.MaxFeeRate) 5402 } 5403 } 5404 } 5405 5406 matchSize := 4 * dcrBtcLotSize 5407 qty := 3 * matchSize 5408 rate := dcrBtcRateStep * 10 5409 lo, dbOrder, preImgL, addr := makeLimitOrder(dc, false, qty, dcrBtcRateStep) 5410 loid := lo.ID() 5411 mid := ordertest.RandomMatchID() 5412 walletSet, _, _, err := tCore.walletSet(dc, tUTXOAssetB.ID, tACCTAsset.ID, false) 5413 if err != nil { 5414 t.Fatalf("walletSet error: %v", err) 5415 } 5416 fundCoinsETH := asset.Coins{&tCoin{id: encode.RandomBytes(36)}} 5417 tEthWallet.fundingCoins = fundCoinsETH 5418 tEthWallet.fundRedeemScripts = []dex.Bytes{nil} 5419 tracker := newTrackedTrade(dbOrder, preImgL, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 5420 rig.db, rig.queue, walletSet, fundCoinsETH, rig.core.notify, rig.core.formatDetails) 5421 rig.dc.trades[tracker.ID()] = tracker 5422 5423 // MAKER REFUND, INVALID TAKER COUNTERSWAP 5424 // 5425 5426 matchTime := time.Now().Truncate(time.Millisecond).UTC() 5427 msgMatch := &msgjson.Match{ 5428 OrderID: loid[:], 5429 MatchID: mid[:], 5430 Quantity: matchSize, 5431 Rate: rate, 5432 Address: "counterparty-address", 5433 Side: uint8(order.Maker), 5434 ServerTime: uint64(matchTime.UnixMilli()), 5435 FeeRateQuote: tMaxFeeRate, 5436 } 5437 swapID := encode.RandomBytes(36) 5438 contract := encode.RandomBytes(36) 5439 tEthWallet.swapReceipts = []asset.Receipt{&tReceipt{coin: &tCoin{id: swapID}, contract: contract}} 5440 sign(tDexPriv, msgMatch) 5441 msg, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch}) 5442 rig.ws.queueResponse(msgjson.InitRoute, initAcker) 5443 err = handleMatchRoute(tCore, rig.dc, msg) 5444 if err != nil { 5445 t.Fatalf("match messages error: %v", err) 5446 } 5447 match, found := tracker.matches[mid] 5448 if !found { 5449 t.Fatalf("match not found") 5450 } 5451 5452 // We're the maker, so the init transaction should be broadcast. 5453 checkStatus("maker swapped", match, order.MakerSwapCast) 5454 proof := &match.MetaData.Proof 5455 if !bytes.Equal(proof.ContractData, contract) { 5456 t.Fatalf("invalid contract recorded for Maker swap") 5457 } 5458 5459 // Send the counter-party's init info. 5460 audit, auditInfo := tMsgAudit(loid, mid, addr, matchSize, proof.SecretHash) 5461 tBtcWallet.auditInfo = auditInfo 5462 auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker).UTC()) 5463 5464 // Check audit errors. 5465 tBtcWallet.auditErr = tErr 5466 err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil) 5467 if err == nil { 5468 t.Fatalf("no maker error for AuditContract error") 5469 } 5470 5471 // Attempt refund. 5472 tEthWallet.refundCoin = encode.RandomBytes(36) 5473 tEthWallet.refundErr = nil 5474 tBtcWallet.refundCoin = nil 5475 tBtcWallet.refundErr = fmt.Errorf("unexpected call to btcWallet.Refund") 5476 matchSizeQuoteUnits := calc.BaseToQuote(rate, matchSize) 5477 // Make the contract appear expired 5478 tEthWallet.contractExpired = true 5479 tEthWallet.contractLockTime = time.Now() 5480 checkRefund(tracker, match, matchSizeQuoteUnits) 5481 tEthWallet.contractExpired = false 5482 tEthWallet.contractLockTime = time.Now().Add(time.Minute) 5483 5484 // TAKER REFUND, NO MAKER REDEEM 5485 // 5486 // Reset funding coins in the trackedTrade, wipe change coin. 5487 matchTime = time.Now().Truncate(time.Millisecond).UTC() 5488 tracker.mtx.Lock() 5489 tracker.coins = mapifyCoins(fundCoinsETH) 5490 tracker.coinsLocked = true 5491 tracker.changeLocked = false 5492 tracker.change = nil 5493 tracker.metaData.ChangeCoin = nil 5494 tracker.mtx.Unlock() 5495 mid = ordertest.RandomMatchID() 5496 msgMatch = &msgjson.Match{ 5497 OrderID: loid[:], 5498 MatchID: mid[:], 5499 Quantity: matchSize, 5500 Rate: rate, 5501 Address: "counterparty-address", 5502 Side: uint8(order.Taker), 5503 ServerTime: uint64(matchTime.UnixMilli()), 5504 FeeRateQuote: tMaxFeeRate, 5505 } 5506 sign(tDexPriv, msgMatch) 5507 msg, _ = msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch}) 5508 err = handleMatchRoute(tCore, rig.dc, msg) 5509 if err != nil { 5510 t.Fatalf("match messages error: %v", err) 5511 } 5512 match, found = tracker.matches[mid] 5513 if !found { 5514 t.Fatalf("match not found") 5515 } 5516 checkStatus("taker matched", match, order.NewlyMatched) 5517 // Send through the audit request for the maker's init. 5518 rig.db.updateMatchChan = make(chan order.MatchStatus, 1) 5519 audit, auditInfo = tMsgAudit(loid, mid, addr, matchSize, nil) 5520 tBtcWallet.auditInfo = auditInfo 5521 auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker)) 5522 tBtcWallet.auditErr = nil 5523 msg, _ = msgjson.NewRequest(1, msgjson.AuditRoute, audit) 5524 err = handleAuditRoute(tCore, rig.dc, msg) 5525 if err != nil { 5526 t.Fatalf("taker's match message error: %v", err) 5527 } 5528 // let the async auditContract run, updating match status 5529 newMatchStatus := <-rig.db.updateMatchChan 5530 if newMatchStatus != order.MakerSwapCast { 5531 t.Fatalf("wrong match status. wanted %v, got %v", order.MakerSwapCast, newMatchStatus) 5532 } 5533 <-rig.db.updateMatchChan // AuditSig set in second update to match data 5534 tracker.mtx.RLock() 5535 if !bytes.Equal(match.MetaData.Proof.Auth.AuditSig, audit.Sig) { 5536 t.Fatalf("audit sig not set for taker") 5537 } 5538 tracker.mtx.RUnlock() 5539 // maker's swap confirmation should trigger taker's swap bcast 5540 tBtcWallet.setConfs(auditInfo.Coin.ID(), tUTXOAssetB.SwapConf, nil) 5541 counterSwapID := encode.RandomBytes(36) 5542 counterScript := encode.RandomBytes(36) 5543 tEthWallet.swapReceipts = []asset.Receipt{&tReceipt{coin: &tCoin{id: counterSwapID}, contract: counterScript}} 5544 rig.ws.queueResponse(msgjson.InitRoute, initAcker) 5545 tCore.tickAsset(dc, tUTXOAssetB.ID) 5546 newMatchStatus = <-rig.db.updateMatchChan // MakerSwapCast->TakerSwapCast (after taker's swap bcast) 5547 if newMatchStatus != order.TakerSwapCast { 5548 t.Fatalf("wrong match status. wanted %v, got %v", order.TakerSwapCast, newMatchStatus) 5549 } 5550 tracker.mtx.RLock() 5551 if !bytes.Equal(match.MetaData.Proof.ContractData, counterScript) { 5552 t.Fatalf("invalid contract recorded for Taker swap") 5553 } 5554 tracker.mtx.RUnlock() 5555 // still takerswapcast, but with initsig 5556 newMatchStatus = <-rig.db.updateMatchChan 5557 if newMatchStatus != order.TakerSwapCast { 5558 t.Fatalf("wrong match status wanted %v, got %v", order.TakerSwapCast, newMatchStatus) 5559 } 5560 tracker.mtx.RLock() 5561 auth := &match.MetaData.Proof.Auth 5562 if len(auth.InitSig) == 0 { 5563 t.Fatalf("init sig not recorded for valid init ack") 5564 } 5565 tracker.mtx.RUnlock() 5566 5567 // Attempt refund. 5568 rig.db.updateMatchChan = nil 5569 tEthWallet.contractExpired = true 5570 tEthWallet.contractLockTime = time.Now() 5571 checkRefund(tracker, match, matchSizeQuoteUnits) 5572 tEthWallet.contractExpired = false 5573 tEthWallet.contractLockTime = time.Now().Add(time.Minute) 5574 } 5575 5576 func TestNotifications(t *testing.T) { 5577 rig := newTestRig() 5578 defer rig.shutdown() 5579 5580 // Insert a notification into the database. 5581 typedNote := newOrderNote("123", "abc", "def", 100, nil) 5582 5583 tCore := rig.core 5584 ch := tCore.NotificationFeed() 5585 tCore.notify(typedNote) 5586 select { 5587 case n := <-ch.C: 5588 dbtest.MustCompareNotifications(t, n.DBNote(), &typedNote.Notification) 5589 case <-time.After(time.Second): 5590 t.Fatalf("no notification received over the notification channel") 5591 } 5592 } 5593 5594 func generateMatch(rig *testRig, baseID, quoteID uint32) (uint64, *order.LimitOrder, *db.MetaOrder, *db.MetaMatch, *tCoin) { 5595 const redemptionReserves = 50 5596 const refundReserves = 75 5597 5598 qty := dcrBtcLotSize * 5 5599 rate := dcrBtcRateStep * 5 5600 lo := &order.LimitOrder{ 5601 P: order.Prefix{ 5602 OrderType: order.LimitOrderType, 5603 BaseAsset: baseID, 5604 QuoteAsset: quoteID, 5605 ClientTime: time.Now(), 5606 ServerTime: time.Now(), 5607 Commit: ordertest.RandomCommitment(), 5608 }, 5609 T: order.Trade{ 5610 Quantity: qty, 5611 Sell: true, 5612 }, 5613 Rate: rate, 5614 Force: order.StandingTiF, // we're calling it booked in OrderMetaData 5615 } 5616 5617 changeCoinID := encode.RandomBytes(36) 5618 changeCoin := &tCoin{id: changeCoinID} 5619 5620 dbOrder := &db.MetaOrder{ 5621 MetaData: &db.OrderMetaData{ 5622 Status: order.OrderStatusBooked, 5623 Host: tDexHost, 5624 Proof: db.OrderProof{}, 5625 ChangeCoin: changeCoinID, 5626 RedemptionReserves: redemptionReserves, 5627 RefundReserves: refundReserves, 5628 }, 5629 Order: lo, 5630 } 5631 5632 oid := lo.ID() 5633 mid := ordertest.RandomMatchID() 5634 addr := ordertest.RandomAddress() 5635 matchQty := qty - dcrBtcLotSize 5636 match := &db.MetaMatch{ 5637 MetaData: &db.MatchMetaData{ 5638 Proof: db.MatchProof{ 5639 CounterContract: encode.RandomBytes(50), 5640 SecretHash: encode.RandomBytes(32), 5641 MakerSwap: encode.RandomBytes(32), 5642 Auth: db.MatchAuth{ 5643 MatchSig: encode.RandomBytes(32), 5644 InitSig: encode.RandomBytes(32), // otherwise MatchComplete will be seen as a cancel order match (inactive) 5645 }, 5646 }, 5647 DEX: tDexHost, 5648 Base: baseID, 5649 Quote: quoteID, 5650 }, 5651 UserMatch: &order.UserMatch{ 5652 OrderID: oid, 5653 MatchID: mid, 5654 Quantity: matchQty, 5655 Rate: rate, 5656 Address: addr, 5657 Status: order.MakerSwapCast, 5658 Side: order.Taker, 5659 }, 5660 } 5661 5662 // Need to return an order from db.ActiveDEXOrders 5663 rig.db.activeDEXOrders = []*db.MetaOrder{dbOrder} 5664 rig.db.orderOrders[oid] = dbOrder 5665 5666 rig.db.activeMatchOIDs = []order.OrderID{oid} 5667 rig.db.matchesForOID = []*db.MetaMatch{match} 5668 5669 return qty, lo, dbOrder, match, changeCoin 5670 } 5671 5672 var ( 5673 activeStatuses = []order.OrderStatus{order.OrderStatusEpoch, order.OrderStatusBooked} 5674 inactiveStatuses = []order.OrderStatus{order.OrderStatusExecuted, order.OrderStatusCanceled, order.OrderStatusRevoked} 5675 5676 reservationTests = []struct { 5677 name string 5678 sell bool 5679 side []order.MatchSide 5680 orderStatuses []order.OrderStatus 5681 matchStatuses []order.MatchStatus 5682 expectedCoins int 5683 }{ 5684 // With an active order, the change coin should always be loaded. 5685 { 5686 name: "active-order, sell", 5687 sell: true, 5688 side: []order.MatchSide{order.Taker, order.Maker}, 5689 orderStatuses: activeStatuses, 5690 matchStatuses: []order.MatchStatus{order.NewlyMatched, order.MakerSwapCast, 5691 order.TakerSwapCast, order.MakerRedeemed, order.MatchComplete}, 5692 expectedCoins: 1, 5693 }, 5694 { 5695 name: "active-order, buy", 5696 sell: false, 5697 side: []order.MatchSide{order.Taker, order.Maker}, 5698 orderStatuses: activeStatuses, 5699 matchStatuses: []order.MatchStatus{order.NewlyMatched, order.MakerSwapCast, 5700 order.TakerSwapCast, order.MakerRedeemed, order.MatchComplete}, 5701 expectedCoins: 1, 5702 }, 5703 // With an inactive order, as taker, if match is >= TakerSwapCast, there 5704 // will be no funding coin fetched. 5705 { 5706 name: "inactive taker > MakerSwapCast, sell", 5707 sell: true, 5708 side: []order.MatchSide{order.Taker}, 5709 orderStatuses: inactiveStatuses, 5710 matchStatuses: []order.MatchStatus{order.TakerSwapCast, order.MakerRedeemed, 5711 order.MatchComplete}, 5712 expectedCoins: 0, 5713 }, 5714 { 5715 name: "inactive taker > MakerSwapCast, buy", 5716 sell: false, 5717 side: []order.MatchSide{order.Taker}, 5718 orderStatuses: inactiveStatuses, 5719 matchStatuses: []order.MatchStatus{order.TakerSwapCast, order.MakerRedeemed, 5720 order.MatchComplete}, 5721 expectedCoins: 0, 5722 }, 5723 // But there will be for NewlyMatched && MakerSwapCast 5724 { 5725 name: "inactive taker < TakerSwapCast, sell", 5726 sell: true, 5727 side: []order.MatchSide{order.Taker}, 5728 orderStatuses: inactiveStatuses, 5729 matchStatuses: []order.MatchStatus{order.NewlyMatched, order.MakerSwapCast}, 5730 expectedCoins: 1, 5731 }, 5732 { 5733 name: "inactive taker < TakerSwapCast, buy", 5734 sell: false, 5735 side: []order.MatchSide{order.Taker}, 5736 orderStatuses: inactiveStatuses, 5737 matchStatuses: []order.MatchStatus{order.NewlyMatched, order.MakerSwapCast}, 5738 expectedCoins: 1, 5739 }, 5740 // For a maker with an inactive order, only NewlyMatched would 5741 // necessitate fetching of coins. 5742 { 5743 name: "inactive maker NewlyMatched, sell", 5744 sell: true, 5745 side: []order.MatchSide{order.Maker}, 5746 orderStatuses: inactiveStatuses, 5747 matchStatuses: []order.MatchStatus{order.NewlyMatched}, 5748 expectedCoins: 1, 5749 }, 5750 { 5751 name: "inactive maker NewlyMatched, buy", 5752 sell: false, 5753 side: []order.MatchSide{order.Maker}, 5754 orderStatuses: inactiveStatuses, 5755 matchStatuses: []order.MatchStatus{order.NewlyMatched}, 5756 expectedCoins: 1, 5757 }, 5758 { 5759 name: "inactive maker > NewlyMatched, sell", 5760 sell: true, 5761 side: []order.MatchSide{order.Maker}, 5762 orderStatuses: inactiveStatuses, 5763 matchStatuses: []order.MatchStatus{order.MakerSwapCast, order.TakerSwapCast, 5764 order.MakerRedeemed, order.MatchComplete}, 5765 expectedCoins: 0, 5766 }, 5767 { 5768 name: "inactive maker > NewlyMatched, buy", 5769 sell: false, 5770 side: []order.MatchSide{order.Maker}, 5771 orderStatuses: inactiveStatuses, 5772 matchStatuses: []order.MatchStatus{order.MakerSwapCast, order.TakerSwapCast, 5773 order.MakerRedeemed, order.MatchComplete}, 5774 expectedCoins: 0, 5775 }, 5776 } 5777 ) 5778 5779 // auth sets the account as authenticated at the provided tier. 5780 func auth(a *dexAccount) { 5781 a.authMtx.Lock() 5782 a.isAuthed = true 5783 a.rep = account.Reputation{BondedTier: 1} 5784 a.authMtx.Unlock() 5785 } 5786 5787 func TestResolveActiveTrades(t *testing.T) { 5788 rig := newTestRig() 5789 defer rig.shutdown() 5790 tCore := rig.core 5791 5792 auth(rig.acct) // Short path through initializeDEXConnections 5793 5794 utxoAsset /* base */, acctAsset /* quote */ := tUTXOAssetB, tACCTAsset 5795 5796 btcWallet, tBtcWallet := newTWallet(utxoAsset.ID) 5797 tCore.wallets[utxoAsset.ID] = btcWallet 5798 5799 ethWallet, tEthWallet := newTAccountLocker(acctAsset.ID) 5800 tCore.wallets[acctAsset.ID] = ethWallet 5801 5802 // Create an order 5803 qty, lo, dbOrder, match, changeCoin := generateMatch(rig, utxoAsset.ID, acctAsset.ID) 5804 redemptionReserves, refundReserves := dbOrder.MetaData.RedemptionReserves, dbOrder.MetaData.RefundReserves 5805 5806 tBtcWallet.fundingCoins = asset.Coins{changeCoin} 5807 tEthWallet.fundingCoins = asset.Coins{changeCoin} 5808 5809 // reset 5810 reset := func() { 5811 rig.acct.lock() 5812 btcWallet.Lock(time.Second) 5813 ethWallet.Lock(time.Second) 5814 tEthWallet.reservedRedemption = 0 5815 tEthWallet.reservedRefund = 0 5816 5817 rig.dc.trades = make(map[order.OrderID]*trackedTrade) 5818 } 5819 5820 // Ensure the order is good, and reset the state. 5821 runTest := func(tag string, expAddedToTradesMap, expReadyToTick, expBTCUnlocked, expETHUnlocked bool, expCoinsLoaded int) { 5822 t.Helper() 5823 defer reset() 5824 5825 description := fmt.Sprintf("%s: side = %s, order status = %s, match status = %s", 5826 tag, match.Side, dbOrder.MetaData.Status, match.Status) 5827 tCore.loginMtx.Lock() 5828 tCore.loggedIn = false 5829 tCore.loginMtx.Unlock() 5830 err := tCore.Login(tPW) 5831 if err != nil { 5832 t.Fatalf("%s: login error: %v", description, err) 5833 } 5834 5835 trade, found := rig.dc.trades[lo.ID()] 5836 if expAddedToTradesMap != found { 5837 t.Fatalf("%s: expected added to trades map = %v, but got %v. len(trades) = %d", description, expAddedToTradesMap, found, len(rig.dc.trades)) 5838 } 5839 if !expAddedToTradesMap { 5840 return 5841 } 5842 5843 if expBTCUnlocked != btcWallet.unlocked() { 5844 t.Fatalf("%s: btc wallet unlocked = %v but got %v", description, expBTCUnlocked, btcWallet.unlocked()) 5845 } 5846 5847 if expETHUnlocked != ethWallet.unlocked() { 5848 t.Fatalf("%s: eth wallet unlocked = %v but got %v", description, expETHUnlocked, ethWallet.unlocked()) 5849 } 5850 5851 _, found = trade.matches[match.MatchID] 5852 if !found { 5853 t.Fatalf("%s: trade with expected order id not found. len(matches) = %d", description, len(trade.matches)) 5854 } 5855 5856 if len(trade.coins) != expCoinsLoaded { 5857 t.Fatalf("%s: expected %d coin loaded, got %d", description, expCoinsLoaded, len(trade.coins)) 5858 } 5859 5860 if found && expReadyToTick != trade.readyToTick { 5861 t.Fatalf("%s: expected ready to tick = %v, but got %v", description, expReadyToTick, trade.readyToTick) 5862 } 5863 if !expReadyToTick { 5864 return 5865 } 5866 5867 if lo.T.Sell && ((match.Side == order.Taker && match.Status < order.MatchComplete) || 5868 (match.Side == order.Taker && match.Status < order.MakerRedeemed)) { 5869 var reReserveQty uint64 = redemptionReserves 5870 if dbOrder.MetaData.Status > order.OrderStatusBooked { 5871 reReserveQty = applyFraction(match.Quantity, qty, redemptionReserves) 5872 } 5873 5874 if tEthWallet.reservedRedemption != reReserveQty { 5875 t.Fatalf("%s: redemption funds not reserved, %d != %d", description, tEthWallet.reservedRedemption, reReserveQty) 5876 } 5877 } 5878 5879 if !lo.T.Sell && match.Status < order.MakerRedeemed { 5880 var reRefundQty uint64 = refundReserves 5881 if dbOrder.MetaData.Status > order.OrderStatusBooked { 5882 reRefundQty = applyFraction(match.Quantity, qty, refundReserves) 5883 } 5884 5885 if tEthWallet.reservedRefund != reRefundQty { 5886 t.Fatalf("%s: refund funds not reserved, %d != %d", description, tEthWallet.reservedRefund, reRefundQty) 5887 } 5888 } 5889 5890 } 5891 5892 runTest("initial", true, true, true, true, 1) 5893 5894 // No base wallet. Trade will not be in the map. 5895 delete(tCore.wallets, utxoAsset.ID) 5896 runTest("no base wallet", false, false, false, false, 0) 5897 tCore.wallets[utxoAsset.ID] = btcWallet 5898 5899 // Base wallet unlock errors. Trade will be in map, but it will not be 5900 // ready to tick. 5901 tBtcWallet.unlockErr = tErr 5902 tBtcWallet.locked = true 5903 runTest("base unlock", true, false, false, false, 0) 5904 tBtcWallet.unlockErr = nil 5905 tBtcWallet.locked = false 5906 5907 // No quote wallet. Trade will not be in the map. 5908 delete(tCore.wallets, acctAsset.ID) 5909 runTest("missing quote", false, false, false, false, 0) 5910 tCore.wallets[acctAsset.ID] = ethWallet 5911 5912 // Quote wallet unlock errors. Trade will be in map, but it will not be 5913 // ready to tick. 5914 tEthWallet.unlockErr = tErr 5915 tEthWallet.locked = true 5916 runTest("quote unlock", true, false, true, false, 0) 5917 tEthWallet.unlockErr = nil 5918 tEthWallet.locked = false 5919 5920 // Funding coin error still puts it in the trades map, and sets ready to tick, 5921 // just with no coins locked. 5922 tBtcWallet.fundingCoinErr = tErr 5923 runTest("funding coin", true, true, true, true, 0) 5924 tBtcWallet.fundingCoinErr = nil 5925 5926 // No matches 5927 rig.db.activeMatchOIDSErr = tErr 5928 runTest("matches error", false, false, false, false, 0) 5929 rig.db.activeMatchOIDSErr = nil 5930 5931 for _, tt := range reservationTests { 5932 lo.T.Sell = tt.sell 5933 if tt.sell { 5934 dbOrder.MetaData.RefundReserves = 0 5935 dbOrder.MetaData.RedemptionReserves = redemptionReserves 5936 } else { 5937 dbOrder.MetaData.RefundReserves = refundReserves 5938 dbOrder.MetaData.RedemptionReserves = 0 5939 } 5940 for _, side := range tt.side { 5941 match.Side = side 5942 for _, orderStatus := range tt.orderStatuses { 5943 dbOrder.MetaData.Status = orderStatus 5944 for _, matchStatus := range tt.matchStatuses { 5945 match.Status = matchStatus 5946 runTest(tt.name, true, true, true, true, tt.expectedCoins) 5947 } 5948 } 5949 } 5950 } 5951 } 5952 5953 func TestReReserveFunding(t *testing.T) { 5954 rig := newTestRig() 5955 defer rig.shutdown() 5956 tCore := rig.core 5957 5958 auth(rig.acct) // Short path through initializeDEXConnections 5959 5960 utxoAsset /* base */, acctAsset /* quote */ := tUTXOAssetB, tACCTAsset 5961 5962 btcWallet, tBtcWallet := newTWallet(utxoAsset.ID) 5963 tCore.wallets[utxoAsset.ID] = btcWallet 5964 5965 ethWallet, tEthWallet := newTAccountLocker(acctAsset.ID) 5966 tCore.wallets[acctAsset.ID] = ethWallet 5967 5968 // Create an order 5969 qty, lo, dbOrder, match, changeCoin := generateMatch(rig, utxoAsset.ID, acctAsset.ID) 5970 redemptionReserves, refundReserves := dbOrder.MetaData.RedemptionReserves, dbOrder.MetaData.RefundReserves 5971 5972 tBtcWallet.fundingCoins = asset.Coins{changeCoin} 5973 tEthWallet.fundingCoins = asset.Coins{changeCoin} 5974 5975 oid := lo.ID() 5976 5977 tracker := &trackedTrade{ 5978 Order: lo, 5979 dc: rig.dc, 5980 metaData: dbOrder.MetaData, 5981 matches: map[order.MatchID]*matchTracker{ 5982 match.MatchID: { 5983 MetaMatch: db.MetaMatch{ 5984 MetaData: &db.MatchMetaData{}, 5985 UserMatch: &order.UserMatch{ 5986 OrderID: lo.ID(), 5987 MatchID: match.MatchID, 5988 Status: order.NewlyMatched, 5989 Side: order.Maker, 5990 Quantity: match.Quantity, 5991 }, 5992 }, 5993 prefix: lo.Prefix(), 5994 trade: lo.Trade(), 5995 // counterConfirms: -1, 5996 }, 5997 }, 5998 coins: map[string]asset.Coin{"changecoinid": changeCoin}, 5999 redemptionReserves: redemptionReserves, 6000 refundReserves: refundReserves, 6001 } 6002 6003 rig.dc.trades = map[order.OrderID]*trackedTrade{ 6004 oid: tracker, 6005 } 6006 6007 // reset 6008 reset := func() { 6009 tEthWallet.reservedRedemption = 0 6010 tEthWallet.reservedRefund = 0 6011 tracker.redemptionLocked = 0 6012 tracker.refundLocked = 0 6013 } 6014 6015 run := func(tag string) { 6016 t.Helper() 6017 description := fmt.Sprintf("%s: side = %s, order status = %s, match status = %s", 6018 tag, match.Side, dbOrder.MetaData.Status, match.Status) 6019 6020 tCore.reReserveFunding(btcWallet) 6021 tCore.reReserveFunding(ethWallet) 6022 6023 if lo.T.Sell && ((match.Side == order.Taker && match.Status < order.MatchComplete) || 6024 (match.Side == order.Taker && match.Status < order.MakerRedeemed)) { 6025 var reReserveQty uint64 = redemptionReserves 6026 if dbOrder.MetaData.Status > order.OrderStatusBooked { 6027 reReserveQty = applyFraction(match.Quantity, qty, redemptionReserves) 6028 } 6029 6030 if tEthWallet.reservedRedemption != reReserveQty { 6031 t.Fatalf("%s: redemption funds not reserved, %d != %d", description, tEthWallet.reservedRedemption, reReserveQty) 6032 } 6033 } 6034 6035 if !lo.T.Sell && match.Status < order.MakerRedeemed { 6036 var reRefundQty uint64 = refundReserves 6037 if dbOrder.MetaData.Status > order.OrderStatusBooked { 6038 reRefundQty = applyFraction(match.Quantity, qty, refundReserves) 6039 } 6040 6041 if tEthWallet.reservedRefund != reRefundQty { 6042 t.Fatalf("%s: refund funds not reserved, %d != %d", description, tEthWallet.reservedRefund, reRefundQty) 6043 } 6044 } 6045 6046 reset() 6047 } 6048 6049 for _, tt := range reservationTests { 6050 6051 lo.T.Sell = tt.sell 6052 tracker.wallets, _, _, _ = tCore.walletSet(rig.dc, utxoAsset.ID, acctAsset.ID, tt.sell) 6053 if tt.sell { 6054 dbOrder.MetaData.RefundReserves = 0 6055 dbOrder.MetaData.RedemptionReserves = redemptionReserves 6056 } else { 6057 dbOrder.MetaData.RefundReserves = refundReserves 6058 dbOrder.MetaData.RedemptionReserves = 0 6059 } 6060 for _, side := range tt.side { 6061 match.Side = side 6062 for _, orderStatus := range tt.orderStatuses { 6063 dbOrder.MetaData.Status = orderStatus 6064 for _, matchStatus := range tt.matchStatuses { 6065 match.Status = matchStatus 6066 run(tt.name) 6067 } 6068 } 6069 } 6070 } 6071 6072 } 6073 6074 func TestCompareServerMatches(t *testing.T) { 6075 rig := newTestRig() 6076 defer rig.shutdown() 6077 preImg := newPreimage() 6078 dc := rig.dc 6079 6080 notes := make(map[string][]Notification) 6081 notify := func(note Notification) { 6082 notes[note.Type()] = append(notes[note.Type()], note) 6083 } 6084 6085 lo := &order.LimitOrder{ 6086 P: order.Prefix{ 6087 // OrderType: order.LimitOrderType, 6088 // BaseAsset: tUTXOAssetA.ID, 6089 // QuoteAsset: tUTXOAssetB.ID, 6090 // ClientTime: time.Now(), 6091 ServerTime: time.Now(), 6092 // Commit: preImg.Commit(), 6093 }, 6094 } 6095 oid := lo.ID() 6096 dbOrder := &db.MetaOrder{ 6097 MetaData: &db.OrderMetaData{}, 6098 Order: lo, 6099 } 6100 tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 6101 rig.db, rig.queue, nil, nil, rig.core.notify, rig.core.formatDetails) 6102 6103 // Known trade, and known match 6104 knownID := ordertest.RandomMatchID() 6105 knownMatch := &matchTracker{ 6106 MetaMatch: db.MetaMatch{ 6107 UserMatch: &order.UserMatch{MatchID: knownID}, 6108 MetaData: &db.MatchMetaData{}, 6109 }, 6110 counterConfirms: -1, 6111 } 6112 tracker.matches[knownID] = knownMatch 6113 knownMsgMatch := &msgjson.Match{OrderID: oid[:], MatchID: knownID[:]} 6114 6115 // Known trade, but missing match 6116 missingID := ordertest.RandomMatchID() 6117 missingMatch := &matchTracker{ 6118 MetaMatch: db.MetaMatch{ 6119 UserMatch: &order.UserMatch{MatchID: missingID}, 6120 MetaData: &db.MatchMetaData{}, 6121 }, 6122 } 6123 tracker.matches[missingID] = missingMatch 6124 6125 // extra match 6126 extraID := ordertest.RandomMatchID() 6127 extraMsgMatch := &msgjson.Match{OrderID: oid[:], MatchID: extraID[:]} 6128 6129 // Entirely missing order 6130 loMissing, dbOrderMissing, preImgMissing, _ := makeLimitOrder(dc, true, 3*dcrBtcLotSize, dcrBtcRateStep*10) 6131 trackerMissing := newTrackedTrade(dbOrderMissing, preImgMissing, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 6132 rig.db, rig.queue, nil, nil, notify, rig.core.formatDetails) 6133 oidMissing := loMissing.ID() 6134 // an active match for the missing trade 6135 matchIDMissing := ordertest.RandomMatchID() 6136 missingTradeMatch := &matchTracker{ 6137 MetaMatch: db.MetaMatch{ 6138 UserMatch: &order.UserMatch{MatchID: matchIDMissing}, 6139 MetaData: &db.MatchMetaData{}, 6140 }, 6141 counterConfirms: -1, 6142 } 6143 trackerMissing.matches[knownID] = missingTradeMatch 6144 // an inactive match for the missing trade 6145 matchIDMissingInactive := ordertest.RandomMatchID() 6146 missingTradeMatchInactive := &matchTracker{ 6147 MetaMatch: db.MetaMatch{ 6148 UserMatch: &order.UserMatch{ 6149 MatchID: matchIDMissingInactive, 6150 Status: order.MatchComplete, 6151 }, 6152 MetaData: &db.MatchMetaData{ 6153 Proof: db.MatchProof{ 6154 Auth: db.MatchAuth{ 6155 RedeemSig: []byte{1, 2, 3}, // won't be considered complete with out it 6156 }, 6157 }, 6158 }, 6159 }, 6160 counterConfirms: 1, 6161 } 6162 trackerMissing.matches[matchIDMissingInactive] = missingTradeMatchInactive 6163 6164 srvMatches := map[order.OrderID]*serverMatches{ 6165 oid: { 6166 tracker: tracker, 6167 msgMatches: []*msgjson.Match{knownMsgMatch, extraMsgMatch}, 6168 }, 6169 // oidMissing not included (missing!) 6170 } 6171 6172 dc.trades = map[order.OrderID]*trackedTrade{ 6173 oid: tracker, 6174 oidMissing: trackerMissing, 6175 } 6176 6177 exceptions, _ := dc.compareServerMatches(srvMatches) 6178 if len(exceptions) != 2 { 6179 t.Fatalf("exceptions did not include both trades, just %d", len(exceptions)) 6180 } 6181 6182 exc, ok := exceptions[oid] 6183 if !ok { 6184 t.Fatalf("exceptions did not include trade %v", oid) 6185 } 6186 if exc.trade.ID() != oid { 6187 t.Fatalf("wrong trade ID, got %v, want %v", exc.trade.ID(), oid) 6188 } 6189 if len(exc.missing) != 1 { 6190 t.Fatalf("found %d missing matches for trade %v, expected 1", len(exc.missing), oid) 6191 } 6192 if exc.missing[0].MatchID != missingID { 6193 t.Fatalf("wrong missing match, got %v, expected %v", exc.missing[0].MatchID, missingID) 6194 } 6195 if len(exc.extra) != 1 { 6196 t.Fatalf("found %d extra matches for trade %v, expected 1", len(exc.extra), oid) 6197 } 6198 if !bytes.Equal(exc.extra[0].MatchID, extraID[:]) { 6199 t.Fatalf("wrong extra match, got %v, expected %v", exc.extra[0].MatchID, extraID) 6200 } 6201 6202 exc, ok = exceptions[oidMissing] 6203 if !ok { 6204 t.Fatalf("exceptions did not include trade %v", oidMissing) 6205 } 6206 if exc.trade.ID() != oidMissing { 6207 t.Fatalf("wrong trade ID, got %v, want %v", exc.trade.ID(), oidMissing) 6208 } 6209 if len(exc.missing) != 1 { // no matchIDMissingInactive 6210 t.Fatalf("found %d missing matches for trade %v, expected 1", len(exc.missing), oid) 6211 } 6212 if exc.missing[0].MatchID != matchIDMissing { 6213 t.Fatalf("wrong missing match, got %v, expected %v", exc.missing[0].MatchID, matchIDMissing) 6214 } 6215 if len(exc.extra) != 0 { 6216 t.Fatalf("found %d extra matches for trade %v, expected 0", len(exc.extra), oid) 6217 } 6218 } 6219 6220 func convertMsgLimitOrder(msgOrder *msgjson.LimitOrder) *order.LimitOrder { 6221 tif := order.ImmediateTiF 6222 if msgOrder.TiF == msgjson.StandingOrderNum { 6223 tif = order.StandingTiF 6224 } 6225 return &order.LimitOrder{ 6226 P: convertMsgPrefix(&msgOrder.Prefix, order.LimitOrderType), 6227 T: convertMsgTrade(&msgOrder.Trade), 6228 Rate: msgOrder.Rate, 6229 Force: tif, 6230 } 6231 } 6232 6233 func convertMsgMarketOrder(msgOrder *msgjson.MarketOrder) *order.MarketOrder { 6234 return &order.MarketOrder{ 6235 P: convertMsgPrefix(&msgOrder.Prefix, order.MarketOrderType), 6236 T: convertMsgTrade(&msgOrder.Trade), 6237 } 6238 } 6239 6240 func convertMsgCancelOrder(msgOrder *msgjson.CancelOrder) *order.CancelOrder { 6241 var oid order.OrderID 6242 copy(oid[:], msgOrder.TargetID) 6243 return &order.CancelOrder{ 6244 P: convertMsgPrefix(&msgOrder.Prefix, order.CancelOrderType), 6245 TargetOrderID: oid, 6246 } 6247 } 6248 6249 func convertMsgPrefix(msgPrefix *msgjson.Prefix, oType order.OrderType) order.Prefix { 6250 var commit order.Commitment 6251 copy(commit[:], msgPrefix.Commit) 6252 var acctID account.AccountID 6253 copy(acctID[:], msgPrefix.AccountID) 6254 return order.Prefix{ 6255 AccountID: acctID, 6256 BaseAsset: msgPrefix.Base, 6257 QuoteAsset: msgPrefix.Quote, 6258 OrderType: oType, 6259 ClientTime: time.UnixMilli(int64(msgPrefix.ClientTime)), 6260 //ServerTime set in epoch queue processing pipeline. 6261 Commit: commit, 6262 } 6263 } 6264 6265 func convertMsgTrade(msgTrade *msgjson.Trade) order.Trade { 6266 coins := make([]order.CoinID, 0, len(msgTrade.Coins)) 6267 for _, coin := range msgTrade.Coins { 6268 var b []byte = coin.ID 6269 coins = append(coins, b) 6270 } 6271 sell := true 6272 if msgTrade.Side == msgjson.BuyOrderNum { 6273 sell = false 6274 } 6275 return order.Trade{ 6276 Coins: coins, 6277 Sell: sell, 6278 Quantity: msgTrade.Quantity, 6279 Address: msgTrade.Address, 6280 } 6281 } 6282 6283 func orderResponse(msgID uint64, msgPrefix msgjson.Stampable, ord order.Order, badSig, noID, badID bool) *msgjson.Message { 6284 orderTime := time.Now() 6285 timeStamp := uint64(orderTime.UnixMilli()) 6286 msgPrefix.Stamp(timeStamp) 6287 sign(tDexPriv, msgPrefix) 6288 if badSig { 6289 msgPrefix.SetSig(encode.RandomBytes(5)) 6290 } 6291 ord.SetTime(orderTime) 6292 oid := ord.ID() 6293 oidB := oid[:] 6294 if noID { 6295 oidB = nil 6296 } else if badID { 6297 oidB = encode.RandomBytes(32) 6298 } 6299 resp, _ := msgjson.NewResponse(msgID, &msgjson.OrderResult{ 6300 Sig: msgPrefix.SigBytes(), 6301 OrderID: oidB, 6302 ServerTime: timeStamp, 6303 }, nil) 6304 return resp 6305 } 6306 6307 func tMsgAudit(oid order.OrderID, mid order.MatchID, recipient string, val uint64, secretHash []byte) (*msgjson.Audit, *asset.AuditInfo) { 6308 auditID := encode.RandomBytes(36) 6309 auditContract := encode.RandomBytes(75) 6310 if secretHash == nil { 6311 secretHash = encode.RandomBytes(32) 6312 } 6313 auditStamp := uint64(time.Now().UnixMilli()) 6314 audit := &msgjson.Audit{ 6315 OrderID: oid[:], 6316 MatchID: mid[:], 6317 Time: auditStamp, 6318 CoinID: auditID, 6319 Contract: auditContract, 6320 } 6321 sign(tDexPriv, audit) 6322 auditCoin := &tCoin{id: auditID, val: val} 6323 auditInfo := &asset.AuditInfo{ 6324 Recipient: recipient, 6325 Coin: auditCoin, 6326 Contract: auditContract, 6327 SecretHash: secretHash, 6328 } 6329 return audit, auditInfo 6330 } 6331 6332 func TestHandleEpochOrderMsg(t *testing.T) { 6333 rig := newTestRig() 6334 defer rig.shutdown() 6335 ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}} 6336 oid := ord.ID() 6337 payload := &msgjson.EpochOrderNote{ 6338 BookOrderNote: msgjson.BookOrderNote{ 6339 OrderNote: msgjson.OrderNote{ 6340 MarketID: tDcrBtcMktName, 6341 OrderID: oid.Bytes(), 6342 }, 6343 TradeNote: msgjson.TradeNote{ 6344 Side: msgjson.BuyOrderNum, 6345 Rate: 4, 6346 Quantity: 10, 6347 }, 6348 }, 6349 Epoch: 1, 6350 } 6351 6352 req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.EpochOrderRoute, payload) 6353 6354 // Ensure handling an epoch order associated with a non-existent orderbook 6355 // generates an error. 6356 err := handleEpochOrderMsg(rig.core, rig.dc, req) 6357 if err == nil { 6358 t.Fatal("[handleEpochOrderMsg] expected a non-existent orderbook error") 6359 } 6360 6361 rig.dc.books[tDcrBtcMktName] = newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger) 6362 6363 err = handleEpochOrderMsg(rig.core, rig.dc, req) 6364 if err != nil { 6365 t.Fatalf("[handleEpochOrderMsg] unexpected error: %v", err) 6366 } 6367 } 6368 6369 func makeMatchProof(preimages []order.Preimage, commitments []order.Commitment) (msgjson.Bytes, msgjson.Bytes, error) { 6370 if len(preimages) != len(commitments) { 6371 return nil, nil, fmt.Errorf("expected equal number of preimages and commitments") 6372 } 6373 6374 sbuff := make([]byte, 0, len(preimages)*order.PreimageSize) 6375 cbuff := make([]byte, 0, len(commitments)*order.CommitmentSize) 6376 for i := 0; i < len(preimages); i++ { 6377 sbuff = append(sbuff, preimages[i][:]...) 6378 cbuff = append(cbuff, commitments[i][:]...) 6379 } 6380 seed := blake256.Sum256(sbuff) 6381 csum := blake256.Sum256(cbuff) 6382 return seed[:], csum[:], nil 6383 } 6384 6385 func TestHandleMatchProofMsg(t *testing.T) { 6386 rig := newTestRig() 6387 defer rig.shutdown() 6388 pimg := newPreimage() 6389 cmt := pimg.Commit() 6390 6391 seed, csum, err := makeMatchProof([]order.Preimage{pimg}, []order.Commitment{cmt}) 6392 if err != nil { 6393 t.Fatalf("[makeMatchProof] unexpected error: %v", err) 6394 } 6395 6396 payload := &msgjson.MatchProofNote{ 6397 MarketID: tDcrBtcMktName, 6398 Epoch: 1, 6399 Preimages: []dex.Bytes{pimg[:]}, 6400 CSum: csum[:], 6401 Seed: seed[:], 6402 } 6403 6404 eo := &msgjson.EpochOrderNote{ 6405 BookOrderNote: msgjson.BookOrderNote{ 6406 OrderNote: msgjson.OrderNote{ 6407 MarketID: tDcrBtcMktName, 6408 OrderID: encode.RandomBytes(order.OrderIDSize), 6409 }, 6410 }, 6411 Epoch: 1, 6412 Commit: cmt[:], 6413 } 6414 6415 req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.MatchProofRoute, payload) 6416 6417 // Ensure match proof validation generates an error for a non-existent orderbook. 6418 err = handleMatchProofMsg(rig.core, rig.dc, req) 6419 if err == nil { 6420 t.Fatal("[handleMatchProofMsg] expected a non-existent orderbook error") 6421 } 6422 6423 rig.dc.books[tDcrBtcMktName] = newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger) 6424 6425 err = rig.dc.books[tDcrBtcMktName].Enqueue(eo) 6426 if err != nil { 6427 t.Fatalf("[Enqueue] unexpected error: %v", err) 6428 } 6429 6430 err = handleMatchProofMsg(rig.core, rig.dc, req) 6431 if err != nil { 6432 t.Fatalf("[handleMatchProofMsg] unexpected error: %v", err) 6433 } 6434 } 6435 6436 func Test_marketTrades(t *testing.T) { 6437 mktID := "dcr_btc" 6438 dc := &dexConnection{ 6439 trades: make(map[order.OrderID]*trackedTrade), 6440 } 6441 6442 preImg := newPreimage() 6443 activeOrd := &order.LimitOrder{P: order.Prefix{ 6444 ServerTime: time.Now(), 6445 Commit: preImg.Commit(), 6446 }} 6447 activeTracker := &trackedTrade{ 6448 Order: activeOrd, 6449 preImg: preImg, 6450 mktID: mktID, 6451 dc: dc, 6452 metaData: &db.OrderMetaData{ 6453 Status: order.OrderStatusBooked, 6454 }, 6455 matches: make(map[order.MatchID]*matchTracker), 6456 } 6457 6458 dc.trades[activeTracker.ID()] = activeTracker 6459 6460 preImg = newPreimage() // different oid 6461 inactiveOrd := &order.LimitOrder{P: order.Prefix{ 6462 ServerTime: time.Now(), 6463 Commit: preImg.Commit(), 6464 }} 6465 inactiveTracker := &trackedTrade{ 6466 Order: inactiveOrd, 6467 preImg: preImg, 6468 mktID: mktID, 6469 dc: dc, 6470 metaData: &db.OrderMetaData{ 6471 Status: order.OrderStatusExecuted, 6472 }, 6473 matches: make(map[order.MatchID]*matchTracker), // no matches 6474 } 6475 6476 dc.trades[inactiveTracker.ID()] = inactiveTracker 6477 6478 trades, _ := dc.marketTrades(mktID) 6479 if len(trades) != 1 { 6480 t.Fatalf("Expected only one trade from marketTrades, found %v", len(trades)) 6481 } 6482 if trades[0].ID() != activeOrd.ID() { 6483 t.Errorf("Expected active order ID %v, got %v", activeOrd.ID(), trades[0].ID()) 6484 } 6485 } 6486 6487 func TestLogout(t *testing.T) { 6488 rig := newTestRig() 6489 defer rig.shutdown() 6490 tCore := rig.core 6491 6492 dcrWallet, _ := newTWallet(tUTXOAssetA.ID) 6493 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 6494 6495 btcWallet, _ := newTWallet(tUTXOAssetB.ID) 6496 tCore.wallets[tUTXOAssetB.ID] = btcWallet 6497 6498 ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}} 6499 tracker := &trackedTrade{ 6500 Order: ord, 6501 preImg: newPreimage(), 6502 dc: rig.dc, 6503 metaData: &db.OrderMetaData{ 6504 Status: order.OrderStatusBooked, 6505 }, 6506 matches: make(map[order.MatchID]*matchTracker), 6507 } 6508 rig.dc.trades[ord.ID()] = tracker 6509 6510 ensureErr := func(tag string) { 6511 t.Helper() 6512 6513 tCore.loginMtx.Lock() 6514 tCore.loggedIn = true 6515 tCore.loginMtx.Unlock() 6516 6517 err := tCore.Logout() 6518 if err == nil { 6519 t.Fatalf("%s: no error", tag) 6520 } 6521 } 6522 6523 // Active orders error. 6524 ensureErr("active orders") 6525 6526 tracker.metaData = &db.OrderMetaData{ 6527 Status: order.OrderStatusExecuted, 6528 } 6529 mid := ordertest.RandomMatchID() 6530 tracker.matches[mid] = &matchTracker{ 6531 MetaMatch: db.MetaMatch{ 6532 MetaData: &db.MatchMetaData{}, 6533 UserMatch: &order.UserMatch{ 6534 OrderID: ord.ID(), 6535 MatchID: mid, 6536 Status: order.NewlyMatched, 6537 Side: order.Maker, 6538 }, 6539 }, 6540 prefix: ord.Prefix(), 6541 trade: ord.Trade(), 6542 counterConfirms: -1, 6543 } 6544 // Active orders with matches error. 6545 ensureErr("active orders matches") 6546 rig.dc.trades = nil 6547 } 6548 6549 func TestSetEpoch(t *testing.T) { 6550 rig := newTestRig() 6551 defer rig.shutdown() 6552 dc := rig.dc 6553 dc.books[tDcrBtcMktName] = newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger) 6554 6555 mktEpoch := func() uint64 { 6556 dc.epochMtx.RLock() 6557 defer dc.epochMtx.RUnlock() 6558 return dc.epoch[tDcrBtcMktName] 6559 } 6560 6561 payload := &msgjson.MatchProofNote{ 6562 MarketID: tDcrBtcMktName, 6563 Epoch: 1, 6564 } 6565 req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.MatchProofRoute, payload) 6566 err := handleMatchProofMsg(rig.core, rig.dc, req) 6567 if err != nil { 6568 t.Fatalf("error advancing epoch: %v", err) 6569 } 6570 if mktEpoch() != 2 { 6571 t.Fatalf("expected epoch 2, got %d", mktEpoch()) 6572 } 6573 6574 payload.Epoch = 0 6575 req, _ = msgjson.NewRequest(rig.dc.NextID(), msgjson.MatchProofRoute, payload) 6576 err = handleMatchProofMsg(rig.core, rig.dc, req) 6577 if err != nil { 6578 t.Fatalf("error handling match proof: %v", err) 6579 } 6580 if mktEpoch() != 2 { 6581 t.Fatalf("epoch changed, expected epoch 2, got %d", mktEpoch()) 6582 } 6583 } 6584 6585 func makeLimitOrder(dc *dexConnection, sell bool, qty, rate uint64) (*order.LimitOrder, *db.MetaOrder, order.Preimage, string) { 6586 preImg := newPreimage() 6587 addr := ordertest.RandomAddress() 6588 lo := &order.LimitOrder{ 6589 P: order.Prefix{ 6590 AccountID: dc.acct.ID(), 6591 BaseAsset: tUTXOAssetA.ID, 6592 QuoteAsset: tUTXOAssetB.ID, 6593 OrderType: order.LimitOrderType, 6594 ClientTime: time.Now(), 6595 ServerTime: time.Now().Add(time.Millisecond), 6596 Commit: preImg.Commit(), 6597 }, 6598 T: order.Trade{ 6599 // Coins needed? 6600 Sell: sell, 6601 Quantity: qty, 6602 Address: addr, 6603 }, 6604 Rate: rate, 6605 Force: order.ImmediateTiF, 6606 } 6607 fromAsset, toAsset := tUTXOAssetB, tUTXOAssetA 6608 if sell { 6609 fromAsset, toAsset = tUTXOAssetA, tUTXOAssetB 6610 } 6611 dbOrder := &db.MetaOrder{ 6612 MetaData: &db.OrderMetaData{ 6613 Status: order.OrderStatusEpoch, 6614 Host: dc.acct.host, 6615 Proof: db.OrderProof{ 6616 Preimage: preImg[:], 6617 }, 6618 MaxFeeRate: tMaxFeeRate, 6619 EpochDur: dc.marketEpochDuration(tDcrBtcMktName), 6620 FromSwapConf: fromAsset.SwapConf, 6621 ToSwapConf: toAsset.SwapConf, 6622 }, 6623 Order: lo, 6624 } 6625 return lo, dbOrder, preImg, addr 6626 } 6627 6628 func TestAddrHost(t *testing.T) { 6629 tests := []struct { 6630 name, addr, want string 6631 wantErr bool 6632 }{{ 6633 name: "scheme, host, and port", 6634 addr: "https://localhost:5758", 6635 want: "localhost:5758", 6636 }, { 6637 name: "scheme, ipv6 host, and port", 6638 addr: "https://[::1]:5758", 6639 want: "[::1]:5758", 6640 }, { 6641 name: "host and port", 6642 addr: "localhost:5758", 6643 want: "localhost:5758", 6644 }, { 6645 name: "just port", 6646 addr: ":5758", 6647 want: "localhost:5758", 6648 }, { 6649 name: "ip host and port", 6650 addr: "127.0.0.1:5758", 6651 want: "127.0.0.1:5758", 6652 }, { 6653 name: "just host", 6654 addr: "thatonedex.com", 6655 want: "thatonedex.com:7232", 6656 }, { 6657 name: "scheme and host", 6658 addr: "https://thatonedex.com", 6659 want: "thatonedex.com:7232", 6660 }, { 6661 name: "scheme, host, and path", 6662 addr: "https://thatonedex.com/any/path", 6663 want: "thatonedex.com:7232", 6664 }, { 6665 name: "ipv6 host", 6666 addr: "[1:2::]", 6667 want: "[1:2::]:7232", 6668 }, { 6669 name: "ipv6 host and port", 6670 addr: "[1:2::]:5758", 6671 want: "[1:2::]:5758", 6672 }, { 6673 name: "empty address", 6674 want: "localhost:7232", 6675 }, { 6676 name: "invalid host", 6677 addr: "https://\n:1234", 6678 wantErr: true, 6679 }, { 6680 name: "invalid port", 6681 addr: ":asdf", 6682 wantErr: true, 6683 }} 6684 for _, test := range tests { 6685 res, err := addrHost(test.addr) 6686 if res != test.want { 6687 t.Fatalf("wanted %s but got %s for test '%s'", test.want, res, test.name) 6688 } 6689 if test.wantErr { 6690 if err == nil { 6691 t.Fatalf("wanted error for test %s, but got none", test.name) 6692 } 6693 continue 6694 } else if err != nil { 6695 t.Fatalf("addrHost error for test %s: %v", test.name, err) 6696 } 6697 // Parsing results a second time should produce the same results. 6698 res, _ = addrHost(res) 6699 if res != test.want { 6700 t.Fatalf("wanted %s but got %s for test '%s'", test.want, res, test.name) 6701 } 6702 } 6703 } 6704 6705 func TestAssetBalance(t *testing.T) { 6706 rig := newTestRig() 6707 defer rig.shutdown() 6708 tCore := rig.core 6709 6710 wallet, tWallet := newTWallet(tUTXOAssetA.ID) 6711 tCore.wallets[tUTXOAssetA.ID] = wallet 6712 bal := &asset.Balance{ 6713 Available: 4e7, 6714 Immature: 6e7, 6715 Locked: 2e8, 6716 } 6717 tWallet.bal = bal 6718 walletBal, err := tCore.AssetBalance(tUTXOAssetA.ID) 6719 if err != nil { 6720 t.Fatalf("error retrieving asset balance: %v", err) 6721 } 6722 dbtest.MustCompareAssetBalances(t, "zero-conf", bal, &walletBal.Balance.Balance) 6723 if walletBal.ContractLocked != 0 { 6724 t.Fatalf("contractlocked balance %d > expected value 0", walletBal.ContractLocked) 6725 } 6726 } 6727 6728 func TestAssetCounter(t *testing.T) { 6729 assets := make(assetMap) 6730 assets.count(1) 6731 if len(assets) != 1 { 6732 t.Fatalf("count not added") 6733 } 6734 6735 newCounts := assetMap{ 6736 1: struct{}{}, 6737 2: struct{}{}, 6738 } 6739 assets.merge(newCounts) 6740 if len(assets) != 2 { 6741 t.Fatalf("counts not absorbed properly") 6742 } 6743 } 6744 6745 func TestHandleTradeSuspensionMsg(t *testing.T) { 6746 rig := newTestRig() 6747 defer rig.shutdown() 6748 6749 tCore := rig.core 6750 dc := rig.dc 6751 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 6752 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 6753 dcrWallet.Unlock(rig.crypter) 6754 6755 btcWallet, _ := newTWallet(tUTXOAssetB.ID) 6756 tCore.wallets[tUTXOAssetB.ID] = btcWallet 6757 btcWallet.Unlock(rig.crypter) 6758 6759 walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true) 6760 6761 rig.dc.books[tDcrBtcMktName] = newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger) 6762 6763 addTracker := func(coins asset.Coins) *trackedTrade { 6764 lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, 0, 0) 6765 oid := lo.ID() 6766 tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 6767 rig.db, rig.queue, walletSet, coins, rig.core.notify, rig.core.formatDetails) 6768 dc.trades[oid] = tracker 6769 return tracker 6770 } 6771 6772 // Make a trade that has a single funding coin, no change coin, and no 6773 // active matches. 6774 fundCoinDcrID := encode.RandomBytes(36) 6775 freshTracker := addTracker(asset.Coins{&tCoin{id: fundCoinDcrID}}) 6776 freshTracker.metaData.Status = order.OrderStatusBooked // suspend with purge only purges book orders since epoch orders are always processed first 6777 6778 // Ensure a non-existent market cannot be suspended. 6779 payload := &msgjson.TradeSuspension{ 6780 MarketID: "dcr_dcr", 6781 } 6782 6783 req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.SuspensionRoute, payload) 6784 err := handleTradeSuspensionMsg(rig.core, rig.dc, req) 6785 if err == nil { 6786 t.Fatal("[handleTradeSuspensionMsg] expected a market ID not found error") 6787 } 6788 6789 newPayload := func() *msgjson.TradeSuspension { 6790 return &msgjson.TradeSuspension{ 6791 MarketID: tDcrBtcMktName, 6792 FinalEpoch: 100, 6793 SuspendTime: uint64(time.Now().Add(time.Millisecond * 20).UnixMilli()), 6794 Persist: false, // Make sure the coins are returned. 6795 } 6796 } 6797 6798 // Suspend a running market. 6799 rig.dc.cfgMtx.Lock() 6800 mktConf := rig.dc.findMarketConfig(tDcrBtcMktName) 6801 mktConf.StartEpoch = 12 6802 rig.dc.cfgMtx.Unlock() 6803 6804 payload = newPayload() 6805 payload.SuspendTime = 0 // now 6806 6807 orderNotes, feedDone := orderNoteFeed(tCore) 6808 defer feedDone() 6809 6810 req, _ = msgjson.NewRequest(rig.dc.NextID(), msgjson.SuspensionRoute, payload) 6811 err = handleTradeSuspensionMsg(rig.core, rig.dc, req) 6812 if err != nil { 6813 t.Fatalf("[handleTradeSuspensionMsg] unexpected error: %v", err) 6814 } 6815 6816 verifyRevokeNotification(orderNotes, TopicOrderAutoRevoked, t) 6817 6818 // Check that the funding coin was returned. Use the tradeMtx for 6819 // synchronization. 6820 dc.tradeMtx.Lock() 6821 if len(tDcrWallet.returnedCoins) != 1 || !bytes.Equal(tDcrWallet.returnedCoins[0].ID(), fundCoinDcrID) { 6822 t.Fatalf("funding coin not returned") 6823 } 6824 dc.tradeMtx.Unlock() 6825 6826 // Make sure the change coin is returned for a trade with a change coin. 6827 delete(dc.trades, freshTracker.ID()) 6828 swappedTracker := addTracker(nil) 6829 changeCoinID := encode.RandomBytes(36) 6830 swappedTracker.change = &tCoin{id: changeCoinID} 6831 swappedTracker.changeLocked = true 6832 swappedTracker.metaData.Status = order.OrderStatusBooked 6833 rig.dc.cfgMtx.Lock() 6834 mktConf.StartEpoch = 12 // make it appear running again first 6835 mktConf.FinalEpoch = 0 6836 mktConf.Persist = nil 6837 rig.dc.cfgMtx.Unlock() 6838 req, _ = msgjson.NewRequest(rig.dc.NextID(), msgjson.SuspensionRoute, payload) 6839 err = handleTradeSuspensionMsg(rig.core, rig.dc, req) 6840 if err != nil { 6841 t.Fatalf("[handleTradeSuspensionMsg] unexpected error: %v", err) 6842 } 6843 // Check that the funding coin was returned. 6844 dc.tradeMtx.Lock() 6845 if len(tDcrWallet.returnedCoins) != 1 || !bytes.Equal(tDcrWallet.returnedCoins[0].ID(), changeCoinID) { 6846 t.Fatalf("change coin not returned") 6847 } 6848 tDcrWallet.returnedCoins = nil 6849 dc.tradeMtx.Unlock() 6850 6851 // Make sure the coin isn't returned if there are unswapped matches. 6852 mid := ordertest.RandomMatchID() 6853 match := &matchTracker{ 6854 MetaMatch: db.MetaMatch{ 6855 UserMatch: &order.UserMatch{MatchID: mid}, // Default status = NewlyMatched 6856 MetaData: &db.MatchMetaData{}, 6857 }, 6858 counterConfirms: -1, 6859 } 6860 swappedTracker.matches[mid] = match 6861 rig.dc.cfgMtx.Lock() 6862 mktConf.StartEpoch = 12 // make it appear running again first 6863 rig.dc.cfgMtx.Unlock() 6864 req, _ = msgjson.NewRequest(rig.dc.NextID(), msgjson.SuspensionRoute, payload) 6865 err = handleTradeSuspensionMsg(rig.core, rig.dc, req) 6866 if err != nil { 6867 t.Fatalf("[handleTradeSuspensionMsg] unexpected error: %v", err) 6868 } 6869 dc.tradeMtx.Lock() 6870 if tDcrWallet.returnedCoins != nil { 6871 t.Fatalf("change coin returned with active matches") 6872 } 6873 dc.tradeMtx.Unlock() 6874 6875 // Ensure trades for a suspended market generate an error. 6876 form := &TradeForm{ 6877 Host: tDexHost, 6878 IsLimit: true, 6879 Sell: true, 6880 Base: tUTXOAssetA.ID, 6881 Quote: tUTXOAssetB.ID, 6882 Qty: dcrBtcLotSize * 10, 6883 Rate: dcrBtcRateStep * 1000, 6884 TifNow: false, 6885 } 6886 6887 _, err = rig.core.Trade(tPW, form) 6888 if err == nil { 6889 t.Fatalf("expected a suspension market error") 6890 } 6891 } 6892 6893 func orderNoteFeed(tCore *Core) (orderNotes chan *OrderNote, done func()) { 6894 orderNotes = make(chan *OrderNote, 16) 6895 6896 ntfnFeed := tCore.NotificationFeed() 6897 feedDone := make(chan struct{}) 6898 var wg sync.WaitGroup 6899 wg.Add(1) 6900 go func() { 6901 defer wg.Done() 6902 for { 6903 select { 6904 case n := <-ntfnFeed.C: 6905 if ordNote, ok := n.(*OrderNote); ok { 6906 orderNotes <- ordNote 6907 } 6908 case <-tCtx.Done(): 6909 return 6910 case <-feedDone: 6911 return 6912 } 6913 } 6914 }() 6915 6916 done = func() { 6917 close(feedDone) // close first on return 6918 wg.Wait() 6919 } 6920 return orderNotes, done 6921 } 6922 6923 func verifyRevokeNotification(ch chan *OrderNote, expectedTopic Topic, t *testing.T) { 6924 t.Helper() 6925 select { 6926 case actualOrderNote := <-ch: 6927 if expectedTopic != actualOrderNote.TopicID { 6928 t.Fatalf("SubjectText mismatch. %s != %s", actualOrderNote.TopicID, 6929 expectedTopic) 6930 } 6931 return 6932 case <-tCtx.Done(): 6933 return 6934 case <-time.After(time.Second): 6935 t.Fatal("timed out waiting for OrderNote notification") 6936 return 6937 } 6938 } 6939 6940 func TestHandleTradeResumptionMsg(t *testing.T) { 6941 rig := newTestRig() 6942 defer rig.shutdown() 6943 6944 tCore := rig.core 6945 dcrWallet, _ := newTWallet(tUTXOAssetA.ID) 6946 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 6947 dcrWallet.Unlock(rig.crypter) 6948 6949 btcWallet, _ := newTWallet(tUTXOAssetB.ID) 6950 tCore.wallets[tUTXOAssetB.ID] = btcWallet 6951 btcWallet.Unlock(rig.crypter) 6952 6953 epochLen := rig.dc.marketConfig(tDcrBtcMktName).EpochLen 6954 6955 handleLimit := func(msg *msgjson.Message, f msgFunc) error { 6956 // Need to stamp and sign the message with the server's key. 6957 msgOrder := new(msgjson.LimitOrder) 6958 err := msg.Unmarshal(msgOrder) 6959 if err != nil { 6960 t.Fatalf("unmarshal error: %v", err) 6961 } 6962 lo := convertMsgLimitOrder(msgOrder) 6963 f(orderResponse(msg.ID, msgOrder, lo, false, false, false)) 6964 return nil 6965 } 6966 6967 tradeForm := &TradeForm{ 6968 Host: tDexHost, 6969 IsLimit: true, 6970 Sell: true, 6971 Base: tUTXOAssetA.ID, 6972 Quote: tUTXOAssetB.ID, 6973 Qty: dcrBtcLotSize * 10, 6974 Rate: dcrBtcRateStep * 1000, 6975 TifNow: false, 6976 } 6977 6978 rig.ws.queueResponse(msgjson.LimitRoute, handleLimit) 6979 6980 // Ensure a non-existent market cannot be suspended. 6981 payload := &msgjson.TradeResumption{ 6982 MarketID: "dcr_dcr", 6983 } 6984 6985 req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.ResumptionRoute, payload) 6986 err := handleTradeResumptionMsg(rig.core, rig.dc, req) 6987 if err == nil { 6988 t.Fatal("[handleTradeResumptionMsg] expected a market ID not found error") 6989 } 6990 6991 var resumeTime uint64 6992 newPayload := func() *msgjson.TradeResumption { 6993 return &msgjson.TradeResumption{ 6994 MarketID: tDcrBtcMktName, 6995 ResumeTime: resumeTime, // set the time to test the scheduling notification case, zero it for immediate resume 6996 StartEpoch: resumeTime / epochLen, 6997 } 6998 } 6999 7000 // Notify of scheduled resume. 7001 rig.dc.cfgMtx.Lock() 7002 mktConf := rig.dc.findMarketConfig(tDcrBtcMktName) 7003 mktConf.StartEpoch = 12 7004 mktConf.FinalEpoch = mktConf.StartEpoch + 1 // long since closed 7005 rig.dc.cfgMtx.Unlock() 7006 7007 resumeTime = uint64(time.Now().Add(time.Hour).UnixMilli()) 7008 payload = newPayload() 7009 req, _ = msgjson.NewRequest(rig.dc.NextID(), msgjson.ResumptionRoute, payload) 7010 err = handleTradeResumptionMsg(rig.core, rig.dc, req) 7011 if err != nil { 7012 t.Fatalf("[handleTradeResumptionMsg] unexpected error: %v", err) 7013 } 7014 7015 // Should be suspended still, no trades 7016 _, err = rig.core.Trade(tPW, tradeForm) 7017 if err == nil { 7018 t.Fatal("trade was accepted for suspended market") 7019 } 7020 7021 // Resume the market immediately. 7022 resumeTime = uint64(time.Now().UnixMilli()) 7023 payload = newPayload() 7024 payload.ResumeTime = 0 // resume now, not scheduled 7025 req, _ = msgjson.NewRequest(rig.dc.NextID(), msgjson.ResumptionRoute, payload) 7026 err = handleTradeResumptionMsg(rig.core, rig.dc, req) 7027 if err != nil { 7028 t.Fatalf("[handleTradeResumptionMsg] unexpected error: %v", err) 7029 } 7030 7031 // Ensure trades for a resumed market are processed without error. 7032 _, err = rig.core.Trade(tPW, tradeForm) 7033 if err != nil { 7034 t.Fatalf("unexpected trade error %v", err) 7035 } 7036 } 7037 7038 func TestHandleNomatch(t *testing.T) { 7039 rig := newTestRig() 7040 defer rig.shutdown() 7041 tCore := rig.core 7042 dc := rig.dc 7043 7044 dcrWallet, _ := newTWallet(tUTXOAssetA.ID) 7045 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 7046 btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) 7047 tCore.wallets[tUTXOAssetB.ID] = btcWallet 7048 7049 walletSet, _, _, err := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true) 7050 if err != nil { 7051 t.Fatalf("walletSet error: %v", err) 7052 } 7053 7054 fundingCoins := asset.Coins{&tCoin{}} 7055 7056 // Four types of order to check 7057 7058 // 1. Immediate limit order 7059 loImmediate, dbOrder, preImgL, _ := makeLimitOrder(dc, true, dcrBtcLotSize*100, dcrBtcRateStep) 7060 loImmediate.Force = order.ImmediateTiF 7061 immediateOID := loImmediate.ID() 7062 immediateTracker := newTrackedTrade(dbOrder, preImgL, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 7063 rig.db, rig.queue, walletSet, fundingCoins, rig.core.notify, rig.core.formatDetails) 7064 dc.trades[immediateOID] = immediateTracker 7065 7066 // 2. Standing limit order 7067 loStanding, dbOrder, preImgL, _ := makeLimitOrder(dc, true, dcrBtcLotSize*100, dcrBtcRateStep) 7068 loStanding.Force = order.StandingTiF 7069 standingOID := loStanding.ID() 7070 standingTracker := newTrackedTrade(dbOrder, preImgL, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 7071 rig.db, rig.queue, walletSet, fundingCoins, rig.core.notify, rig.core.formatDetails) 7072 dc.trades[standingOID] = standingTracker 7073 7074 // 3. Cancel order. 7075 cancelOrder := &order.CancelOrder{ 7076 P: order.Prefix{ 7077 ServerTime: time.Now(), 7078 }, 7079 } 7080 cancelOID := cancelOrder.ID() 7081 standingTracker.cancel = &trackedCancel{ 7082 CancelOrder: *cancelOrder, 7083 } 7084 dc.registerCancelLink(cancelOID, standingOID) 7085 7086 // 4. Market order. 7087 loWillBeMarket, dbOrder, preImgL, _ := makeLimitOrder(dc, true, dcrBtcLotSize*100, dcrBtcRateStep) 7088 mktOrder := &order.MarketOrder{ 7089 P: loWillBeMarket.P, 7090 T: *loWillBeMarket.Trade().Copy(), 7091 } 7092 dbOrder.Order = mktOrder 7093 marketOID := mktOrder.ID() 7094 marketTracker := newTrackedTrade(dbOrder, preImgL, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 7095 rig.db, rig.queue, walletSet, fundingCoins, rig.core.notify, rig.core.formatDetails) 7096 dc.trades[marketOID] = marketTracker 7097 7098 runNomatch := func(tag string, oid order.OrderID) { 7099 tracker, _ := dc.findOrder(oid) 7100 if tracker == nil { 7101 t.Fatalf("%s: order ID not found", tag) 7102 } 7103 payload := &msgjson.NoMatch{OrderID: oid[:]} 7104 req, _ := msgjson.NewRequest(dc.NextID(), msgjson.NoMatchRoute, payload) 7105 err := handleNoMatchRoute(tCore, dc, req) 7106 if err != nil { 7107 t.Fatalf("handleNoMatchRoute error: %v", err) 7108 } 7109 } 7110 7111 checkTradeStatus := func(tag string, oid order.OrderID, expStatus order.OrderStatus) { 7112 tracker, _ := dc.findOrder(oid) 7113 if tracker.metaData.Status != expStatus { 7114 t.Fatalf("%s: wrong status. expected %s, got %s", tag, expStatus, tracker.metaData.Status) 7115 } 7116 if rig.db.lastStatusID != oid { 7117 t.Fatalf("%s: order status not stored", tag) 7118 } 7119 if rig.db.lastStatus != expStatus { 7120 t.Fatalf("%s: wrong order status stored. expected %s, got %s", tag, expStatus, rig.db.lastStatus) 7121 } 7122 if expStatus == order.OrderStatusExecuted { 7123 if tBtcWallet.returnedAddr != tracker.Trade().Address { 7124 t.Fatalf("%s: redemption address not returned", tag) 7125 } 7126 } 7127 } 7128 7129 runNomatch("cancel", cancelOID) 7130 if rig.db.lastStatusID != cancelOID || rig.db.lastStatus != order.OrderStatusExecuted { 7131 t.Fatalf("cancel status not updated") 7132 } 7133 if rig.db.linkedFromID != standingOID || !rig.db.linkedToID.IsZero() { 7134 t.Fatalf("missed cancel not unlinked. wanted trade ID %s, got %s. wanted zeroed linked ID, got %s", 7135 standingOID, rig.db.linkedFromID, rig.db.linkedToID) 7136 } 7137 7138 runNomatch("standing limit", standingOID) 7139 checkTradeStatus("standing limit", standingOID, order.OrderStatusBooked) 7140 7141 runNomatch("immediate", immediateOID) 7142 checkTradeStatus("immediate", immediateOID, order.OrderStatusExecuted) 7143 7144 runNomatch("market", marketOID) 7145 checkTradeStatus("market", marketOID, order.OrderStatusExecuted) 7146 7147 // Unknown order should error. 7148 oid := ordertest.RandomOrderID() 7149 payload := &msgjson.NoMatch{OrderID: oid[:]} 7150 req, _ := msgjson.NewRequest(dc.NextID(), msgjson.NoMatchRoute, payload) 7151 err = handleNoMatchRoute(tCore, dc, req) 7152 if !errorHasCode(err, unknownOrderErr) { 7153 t.Fatalf("wrong error for unknown order ID: %v", err) 7154 } 7155 } 7156 7157 func TestWalletSettings(t *testing.T) { 7158 rig := newTestRig() 7159 defer rig.shutdown() 7160 tCore := rig.core 7161 rig.db.wallet = &db.Wallet{ 7162 Settings: map[string]string{ 7163 "abc": "123", 7164 }, 7165 } 7166 var assetID uint32 = 54321 7167 7168 // wallet not found 7169 _, err := tCore.WalletSettings(assetID) 7170 if !errorHasCode(err, missingWalletErr) { 7171 t.Fatalf("wrong error for missing wallet: %v", err) 7172 } 7173 7174 tCore.wallets[assetID] = &xcWallet{} 7175 7176 // db error 7177 rig.db.walletErr = tErr 7178 _, err = tCore.WalletSettings(assetID) 7179 if !errorHasCode(err, dbErr) { 7180 t.Fatalf("wrong error when expected db error: %v", err) 7181 } 7182 rig.db.walletErr = nil 7183 7184 // success 7185 returnedSettings, err := tCore.WalletSettings(assetID) 7186 if err != nil { 7187 t.Fatalf("WalletSettings error: %v", err) 7188 } 7189 7190 if len(returnedSettings) != 1 || returnedSettings["abc"] != "123" { 7191 t.Fatalf("returned wallet settings are not correct: %v", returnedSettings) 7192 } 7193 } 7194 7195 func TestChangeAppPass(t *testing.T) { 7196 rig := newTestRig() 7197 defer rig.shutdown() 7198 // Use the smarter crypter. 7199 smartCrypter := newTCrypterSmart() 7200 rig.crypter = smartCrypter 7201 rig.core.newCrypter = func([]byte) encrypt.Crypter { return newTCrypterSmart() } 7202 rig.core.reCrypter = func([]byte, []byte) (encrypt.Crypter, error) { return rig.crypter, smartCrypter.recryptErr } 7203 7204 tCore := rig.core 7205 newTPW := []byte("apppass") 7206 7207 // App Password error 7208 rig.crypter.(*tCrypterSmart).recryptErr = tErr 7209 err := tCore.ChangeAppPass(tPW, newTPW) 7210 if !errorHasCode(err, authErr) { 7211 t.Fatalf("wrong error for password error: %v", err) 7212 } 7213 rig.crypter.(*tCrypterSmart).recryptErr = nil 7214 7215 oldCreds := tCore.credentials 7216 7217 rig.db.creds = nil 7218 err = tCore.ChangeAppPass(tPW, newTPW) 7219 if err != nil { 7220 t.Fatal(err) 7221 } 7222 7223 if bytes.Equal(oldCreds.OuterKeyParams, tCore.credentials.OuterKeyParams) { 7224 t.Fatalf("credentials not updated in Core") 7225 } 7226 7227 if rig.db.creds == nil || !bytes.Equal(tCore.credentials.OuterKeyParams, rig.db.creds.OuterKeyParams) { 7228 t.Fatalf("credentials not updated in DB") 7229 } 7230 } 7231 7232 func TestResetAppPass(t *testing.T) { 7233 rig := newTestRig() 7234 defer rig.shutdown() 7235 crypter := newTCrypterSmart() 7236 rig.crypter = crypter 7237 rig.core.newCrypter = func([]byte) encrypt.Crypter { return crypter } 7238 rig.core.reCrypter = func([]byte, []byte) (encrypt.Crypter, error) { return rig.crypter, crypter.recryptErr } 7239 7240 rig.core.credentials = nil 7241 rig.core.InitializeClient(tPW, nil) 7242 7243 tCore := rig.core 7244 seed, err := tCore.ExportSeed(tPW) 7245 if err != nil { 7246 t.Fatalf("seed export failed: %v", err) 7247 } 7248 7249 // Invalid seed error 7250 invalidSeed := seed[:24] 7251 err = tCore.ResetAppPass(tPW, invalidSeed) 7252 if !strings.Contains(err.Error(), "unabled to decode provided seed") { 7253 t.Fatalf("wrong error for invalid seed length: %v", err) 7254 } 7255 7256 // Want incorrect seed error. 7257 rig.crypter.(*tCrypterSmart).recryptErr = tErr 7258 // tCrypter is used to encode the orginal seed but we don't need it here, so 7259 // we need to add 8 bytes to commplete the expected seed lenght(64). 7260 err = tCore.ResetAppPass(tPW, seed+"blah") 7261 if !strings.Contains(err.Error(), "unabled to decode provided seed") { 7262 t.Fatalf("wrong error for incorrect seed: %v", err) 7263 } 7264 7265 // ok, no crypter error. 7266 rig.crypter.(*tCrypterSmart).recryptErr = nil 7267 err = tCore.ResetAppPass(tPW, seed) 7268 if err != nil { 7269 t.Fatalf("unexpected error: %v", err) 7270 } 7271 } 7272 7273 func TestReconfigureWallet(t *testing.T) { 7274 rig := newTestRig() 7275 defer rig.shutdown() 7276 tCore := rig.core 7277 rig.db.wallet = &db.Wallet{ 7278 Settings: map[string]string{ 7279 "abc": "123", 7280 }, 7281 } 7282 const assetID uint32 = 54321 7283 xyzWallet, tXyzWallet := newTWallet(assetID) 7284 newSettings := map[string]string{ 7285 "def": "456", 7286 } 7287 7288 form := &WalletForm{ 7289 AssetID: assetID, 7290 Config: newSettings, 7291 Type: "type", 7292 } 7293 xyzWallet.walletType = "type" 7294 7295 // App Password error 7296 rig.crypter.(*tCrypter).recryptErr = tErr 7297 err := tCore.ReconfigureWallet(tPW, nil, form) 7298 if !errorHasCode(err, authErr) { 7299 t.Fatalf("wrong error for password error: %v", err) 7300 } 7301 rig.crypter.(*tCrypter).recryptErr = nil 7302 7303 // Missing wallet error 7304 err = tCore.ReconfigureWallet(tPW, nil, form) 7305 if !errorHasCode(err, assetSupportErr) { 7306 t.Fatalf("wrong error for missing wallet definition: %v", err) 7307 } 7308 7309 walletDef := &asset.WalletDefinition{ 7310 Type: "type", 7311 Seeded: true, 7312 } 7313 winfo := *tWalletInfo 7314 winfo.AvailableWallets = []*asset.WalletDefinition{walletDef} 7315 7316 assetDriver := &tCreator{ 7317 tDriver: &tDriver{ 7318 wallet: xyzWallet.Wallet, 7319 winfo: &winfo, 7320 }, 7321 } 7322 asset.Register(assetID, assetDriver) 7323 if err = xyzWallet.Connect(); err != nil { 7324 t.Fatal(err) 7325 } 7326 defer xyzWallet.Disconnect() 7327 7328 // Missing wallet error 7329 err = tCore.ReconfigureWallet(tPW, nil, form) 7330 if !errorHasCode(err, missingWalletErr) { 7331 t.Fatalf("wrong error for missing wallet: %v", err) 7332 } 7333 7334 tCore.wallets[assetID] = xyzWallet 7335 7336 // Errors for seeded wallets. 7337 walletDef.Seeded = true 7338 // Exists error 7339 assetDriver.existsErr = tErr 7340 err = tCore.ReconfigureWallet(tPW, nil, form) 7341 if !errorHasCode(err, existenceCheckErr) { 7342 t.Fatalf("wrong error when expecting existence check error: %v", err) 7343 } 7344 assetDriver.existsErr = nil 7345 // Create error 7346 assetDriver.doesntExist = true 7347 assetDriver.createErr = tErr 7348 err = tCore.ReconfigureWallet(tPW, nil, form) 7349 if !errorHasCode(err, createWalletErr) { 7350 t.Fatalf("wrong error when expecting wallet creation error error: %v", err) 7351 } 7352 assetDriver.createErr = nil 7353 walletDef.Seeded = false 7354 7355 // Connect error 7356 tXyzWallet.connectErr = tErr 7357 err = tCore.ReconfigureWallet(tPW, nil, form) 7358 if !errorHasCode(err, connectWalletErr) { 7359 t.Fatalf("wrong error when expecting connection error: %v", err) 7360 } 7361 tXyzWallet.connectErr = nil 7362 7363 // Unlock error 7364 tXyzWallet.Unlock(wPW) 7365 tXyzWallet.unlockErr = tErr 7366 err = tCore.ReconfigureWallet(tPW, nil, form) 7367 if !errorHasCode(err, walletAuthErr) { 7368 t.Fatalf("wrong error when expecting auth error: %v", err) 7369 } 7370 tXyzWallet.unlockErr = nil 7371 7372 // For the last success, make sure that we also clear any related 7373 // tickGovernors. 7374 abcWallet, _ := newTWallet(tUTXOAssetA.ID) // for to/baseWallet 7375 matchID := ordertest.RandomMatchID() 7376 match := &matchTracker{ 7377 suspectSwap: true, 7378 tickGovernor: time.NewTimer(time.Hour), 7379 MetaMatch: db.MetaMatch{ 7380 MetaData: &db.MatchMetaData{ 7381 Proof: db.MatchProof{ 7382 ContractData: dex.Bytes{0}, 7383 }, 7384 }, 7385 UserMatch: &order.UserMatch{ 7386 MatchID: matchID, 7387 }, 7388 }, 7389 } 7390 tCore.conns[tDexHost].tradeMtx.Lock() 7391 tCore.conns[tDexHost].trades[order.OrderID{}] = &trackedTrade{ 7392 Order: &order.LimitOrder{ 7393 P: order.Prefix{ 7394 BaseAsset: assetID, 7395 ServerTime: time.Now(), 7396 }, 7397 }, 7398 wallets: &walletSet{ 7399 fromWallet: xyzWallet, 7400 quoteWallet: xyzWallet, // sell=false 7401 toWallet: abcWallet, 7402 baseWallet: abcWallet, 7403 }, 7404 matches: map[order.MatchID]*matchTracker{ 7405 {}: match, 7406 }, 7407 metaData: &db.OrderMetaData{}, 7408 dc: rig.dc, 7409 readyToTick: true, // prevent resume path 7410 } 7411 tCore.conns[tDexHost].tradeMtx.Unlock() 7412 7413 // Error checking if wallet owns address. 7414 tXyzWallet.ownsAddressErr = tErr 7415 err = tCore.ReconfigureWallet(tPW, nil, form) 7416 if !errorHasCode(err, walletErr) { 7417 t.Fatalf("wrong error when expecting ownsAddress wallet error: %v", err) 7418 } 7419 tXyzWallet.ownsAddressErr = nil 7420 7421 // Wallet doesn't own address. 7422 tXyzWallet.ownsAddress = false 7423 err = tCore.ReconfigureWallet(tPW, nil, form) 7424 if !errorHasCode(err, walletErr) { 7425 t.Fatalf("wrong error when expecting not owned wallet error: %v", err) 7426 } 7427 7428 // Leave the ownsAddress false, but swap out a LiveReconfigurer and ensure 7429 // the restart = false path passes. 7430 liveReconfigurer := &TLiveReconfigurer{TXCWallet: tXyzWallet} 7431 xyzWallet.Wallet = liveReconfigurer 7432 if err = tCore.ReconfigureWallet(tPW, nil, form); err != nil { 7433 t.Fatalf("ReconfigureWallet error for short path: %v", err) 7434 } 7435 7436 // But restart = true should still fail for live orders. 7437 liveReconfigurer.restart = true 7438 err = tCore.ReconfigureWallet(tPW, nil, form) 7439 if !errorHasCode(err, walletErr) { 7440 t.Fatalf("wrong error when expecting not owned wallet error: %v", err) 7441 } 7442 liveReconfigurer.restart = false 7443 7444 // OwnsAddress error 7445 liveReconfigurer.ownsAddressErr = tErr 7446 err = tCore.ReconfigureWallet(tPW, nil, form) 7447 if !errorHasCode(err, walletErr) { 7448 t.Fatalf("wrong error when expecting ownsAddress wallet error without restart: %v", err) 7449 } 7450 liveReconfigurer.ownsAddressErr = nil 7451 7452 // Refresh address error 7453 liveReconfigurer.addrErr = tErr 7454 err = tCore.ReconfigureWallet(tPW, nil, form) 7455 if !errorHasCode(err, newAddrErr) { 7456 t.Fatalf("wrong error when expecting address refresh error without restart: %v", err) 7457 } 7458 liveReconfigurer.addrErr = nil 7459 7460 // Password error for non-seeded wallet with password. 7461 // from above: walletDef.Seeded = false 7462 liveReconfigurer.unlockErr = tErr 7463 err = tCore.ReconfigureWallet(tPW, append(tPW, 5), form) 7464 if !errorHasCode(err, walletAuthErr) { 7465 t.Fatalf("wrong error when expecting new password error without restart: %v", err) 7466 } 7467 liveReconfigurer.unlockErr = nil 7468 7469 // DB error for restartless path. 7470 rig.db.updateWalletErr = tErr 7471 err = tCore.ReconfigureWallet(tPW, nil, form) 7472 if !errorHasCode(err, dbErr) { 7473 t.Fatalf("wrong error when db update error without restart: %v", err) 7474 } 7475 rig.db.updateWalletErr = nil 7476 7477 // End LiveReconfigurer tests. 7478 xyzWallet.Wallet = tXyzWallet 7479 tXyzWallet.ownsAddress = true 7480 7481 // Success updating settings. 7482 err = tCore.ReconfigureWallet(tPW, nil, form) 7483 if err != nil { 7484 t.Fatalf("ReconfigureWallet error: %v", err) 7485 } 7486 7487 settings := rig.db.wallet.Settings 7488 if len(settings) != 1 || settings["def"] != "456" { 7489 t.Fatalf("settings not stored") 7490 } 7491 7492 if match.tickGovernor != nil { 7493 t.Fatalf("tickGovernor not removed") 7494 } 7495 7496 // Success updating wallet PW. 7497 newWalletPW := []byte("password") 7498 err = tCore.ReconfigureWallet(tPW, newWalletPW, form) 7499 if err != nil { 7500 t.Fatalf("ReconfigureWallet error: %v", err) 7501 } 7502 7503 // Check that the xcWallet was updated. 7504 xyzWallet = tCore.wallets[assetID] 7505 decNewPW, _ := rig.crypter.Decrypt(xyzWallet.encPW()) 7506 if !bytes.Equal(decNewPW, newWalletPW) { 7507 t.Fatalf("xcWallet encPW field not updated want: %x got: %x", 7508 newWalletPW, decNewPW) 7509 } 7510 } 7511 7512 func TestSetWalletPassword(t *testing.T) { 7513 rig := newTestRig() 7514 defer rig.shutdown() 7515 tCore := rig.core 7516 rig.db.wallet = &db.Wallet{ 7517 EncryptedPW: []byte("abc"), 7518 } 7519 newPW := []byte("def") 7520 var assetID uint32 = 54321 7521 7522 // Nil password error 7523 err := tCore.SetWalletPassword(tPW, assetID, nil) 7524 if !errorHasCode(err, passwordErr) { 7525 t.Fatalf("wrong error for nil password error: %v", err) 7526 } 7527 7528 // Auth error 7529 rig.crypter.(*tCrypter).recryptErr = tErr 7530 err = tCore.SetWalletPassword(tPW, assetID, newPW) 7531 if !errorHasCode(err, authErr) { 7532 t.Fatalf("wrong error for auth error: %v", err) 7533 } 7534 rig.crypter.(*tCrypter).recryptErr = nil 7535 7536 // Missing wallet error 7537 err = tCore.SetWalletPassword(tPW, assetID, newPW) 7538 if !errorHasCode(err, missingWalletErr) { 7539 t.Fatalf("wrong error for missing wallet: %v", err) 7540 } 7541 7542 xyzWallet, tXyzWallet := newTWallet(assetID) 7543 tCore.wallets[assetID] = xyzWallet 7544 7545 // Connection error 7546 xyzWallet.hookedUp = false 7547 tXyzWallet.connectErr = tErr 7548 err = tCore.SetWalletPassword(tPW, assetID, newPW) 7549 if !errorHasCode(err, connectionErr) { 7550 t.Fatalf("wrong error for connection error: %v", err) 7551 } 7552 xyzWallet.hookedUp = true 7553 tXyzWallet.connectErr = nil 7554 7555 // Unlock error 7556 tXyzWallet.unlockErr = tErr 7557 err = tCore.SetWalletPassword(tPW, assetID, newPW) 7558 if !errorHasCode(err, authErr) { 7559 t.Fatalf("wrong error for auth error: %v", err) 7560 } 7561 tXyzWallet.unlockErr = nil 7562 7563 // SetWalletPassword db error 7564 rig.db.setWalletPwErr = tErr 7565 err = tCore.SetWalletPassword(tPW, assetID, newPW) 7566 if !errorHasCode(err, dbErr) { 7567 t.Fatalf("wrong error for missing wallet: %v", err) 7568 } 7569 rig.db.setWalletPwErr = nil 7570 7571 // Success 7572 err = tCore.SetWalletPassword(tPW, assetID, newPW) 7573 if err != nil { 7574 t.Fatalf("SetWalletPassword error: %v", err) 7575 } 7576 7577 // Check that the xcWallet was updated. 7578 decNewPW, _ := rig.crypter.Decrypt(xyzWallet.encPW()) 7579 if !bytes.Equal(decNewPW, newPW) { 7580 t.Fatalf("xcWallet encPW field not updated") 7581 } 7582 } 7583 7584 func TestHandlePenaltyMsg(t *testing.T) { 7585 rig := newTestRig() 7586 defer rig.shutdown() 7587 tCore := rig.core 7588 dc := rig.dc 7589 penalty := &msgjson.Penalty{ 7590 Rule: account.Rule(1), 7591 Time: uint64(1598929305), 7592 Details: "You may no longer trade. Leave your client running to finish pending trades.", 7593 } 7594 diffKey, _ := secp256k1.GeneratePrivateKey() 7595 noMatch, err := msgjson.NewNotification(msgjson.NoMatchRoute, "fake") 7596 if err != nil { 7597 t.Fatal(err) 7598 } 7599 tests := []struct { 7600 name string 7601 key *secp256k1.PrivateKey 7602 payload any 7603 wantErr bool 7604 }{{ 7605 name: "ok", 7606 key: tDexPriv, 7607 payload: penalty, 7608 }, { 7609 name: "bad note", 7610 key: tDexPriv, 7611 payload: noMatch, 7612 wantErr: true, 7613 }, { 7614 name: "wrong sig", 7615 key: diffKey, 7616 payload: penalty, 7617 wantErr: true, 7618 }} 7619 for _, test := range tests { 7620 var err error 7621 var note *msgjson.Message 7622 switch v := test.payload.(type) { 7623 case *msgjson.Penalty: 7624 penaltyNote := &msgjson.PenaltyNote{ 7625 Penalty: v, 7626 } 7627 sign(test.key, penaltyNote) 7628 note, err = msgjson.NewNotification(msgjson.PenaltyRoute, penaltyNote) 7629 if err != nil { 7630 t.Fatalf("error creating penalty notification: %v", err) 7631 } 7632 case *msgjson.Message: 7633 note = v 7634 default: 7635 t.Fatalf("unknown payload type: %T", v) 7636 } 7637 7638 err = handlePenaltyMsg(tCore, dc, note) 7639 if test.wantErr { 7640 if err == nil { 7641 t.Fatalf("expected error for test %s", test.name) 7642 } 7643 continue 7644 } 7645 if err != nil { 7646 t.Fatalf("%s: unexpected error: %v", test.name, err) 7647 } 7648 } 7649 } 7650 7651 func TestPreimageSync(t *testing.T) { 7652 rig := newTestRig() 7653 defer rig.shutdown() 7654 tCore := rig.core 7655 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 7656 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 7657 dcrWallet.Unlock(rig.crypter) 7658 7659 btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) 7660 tCore.wallets[tUTXOAssetB.ID] = btcWallet 7661 btcWallet.Unlock(rig.crypter) 7662 7663 var lots uint64 = 10 7664 qty := dcrBtcLotSize * lots 7665 rate := dcrBtcRateStep * 1000 7666 7667 form := &TradeForm{ 7668 Host: tDexHost, 7669 IsLimit: true, 7670 Sell: true, 7671 Base: tUTXOAssetA.ID, 7672 Quote: tUTXOAssetB.ID, 7673 Qty: qty, 7674 Rate: rate, 7675 TifNow: false, 7676 } 7677 7678 dcrCoin := &tCoin{ 7679 id: encode.RandomBytes(36), 7680 val: qty * 2, 7681 } 7682 tDcrWallet.fundingCoins = asset.Coins{dcrCoin} 7683 tDcrWallet.fundRedeemScripts = []dex.Bytes{nil} 7684 7685 btcVal := calc.BaseToQuote(rate, qty*2) 7686 btcCoin := &tCoin{ 7687 id: encode.RandomBytes(36), 7688 val: btcVal, 7689 } 7690 tBtcWallet.fundingCoins = asset.Coins{btcCoin} 7691 tBtcWallet.fundRedeemScripts = []dex.Bytes{nil} 7692 7693 limitRouteProcessing := make(chan order.OrderID) 7694 var commit order.Commitment 7695 7696 rig.ws.queueResponse(msgjson.LimitRoute, func(msg *msgjson.Message, f msgFunc) error { 7697 t.Helper() 7698 // Need to stamp and sign the message with the server's key. 7699 msgOrder := new(msgjson.LimitOrder) 7700 err := msg.Unmarshal(msgOrder) 7701 if err != nil { 7702 return fmt.Errorf("unmarshal error: %w", err) 7703 } 7704 lo := convertMsgLimitOrder(msgOrder) 7705 resp := orderResponse(msg.ID, msgOrder, lo, false, false, false) 7706 limitRouteProcessing <- lo.ID() 7707 commit = lo.Commit // accessed below only after errChan receive indicating Trade done 7708 f(resp) // e.g. the UnmarshalJSON in sendRequest 7709 return nil 7710 }) 7711 7712 errChan := make(chan error, 1) 7713 // Run the trade in a goroutine. 7714 go func() { 7715 _, err := tCore.Trade(tPW, form) 7716 errChan <- err 7717 }() 7718 7719 // Wait for the limit route to start processing. Then we have 100 ms to call 7720 // handlePreimageRequest to catch the early-preimage case. 7721 var oid order.OrderID 7722 select { 7723 case oid = <-limitRouteProcessing: 7724 case <-time.After(time.Second): 7725 t.Fatalf("limit route never hit") 7726 } 7727 7728 err := <-errChan 7729 if err != nil { 7730 t.Fatalf("trade error: %v", err) 7731 } 7732 7733 // So ideally, we're calling handlePreimageRequest about 100 ms before we 7734 // even have an order id back from the server. This shouldn't result in an 7735 // error. 7736 payload := &msgjson.PreimageRequest{ 7737 OrderID: oid[:], 7738 Commitment: commit[:], 7739 } 7740 req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload) 7741 err = handlePreimageRequest(rig.core, rig.dc, req) 7742 if err != nil { 7743 t.Fatalf("early preimage request error: %v", err) 7744 } 7745 } 7746 7747 func TestAccelerateOrder(t *testing.T) { 7748 rig := newTestRig() 7749 defer rig.shutdown() 7750 tCore := rig.core 7751 dc := rig.dc 7752 7753 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 7754 tDcrWallet.swapSize = tSwapSizeA 7755 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 7756 btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) 7757 tBtcWallet.swapSize = tSwapSizeB 7758 tCore.wallets[tUTXOAssetB.ID] = btcWallet 7759 7760 buyWalletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, false) 7761 sellWalletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, false) 7762 7763 var newBaseFeeRate uint64 = 55 7764 var newQuoteFeeRate uint64 = 65 7765 feeRateSource := func(msg *msgjson.Message, f msgFunc) error { 7766 var resp *msgjson.Message 7767 if string(msg.Payload) == "42" { 7768 resp, _ = msgjson.NewResponse(msg.ID, newBaseFeeRate, nil) 7769 } else { 7770 resp, _ = msgjson.NewResponse(msg.ID, newQuoteFeeRate, nil) 7771 } 7772 f(resp) 7773 return nil 7774 } 7775 7776 type testMatch struct { 7777 status order.MatchStatus 7778 quantity uint64 7779 rate uint64 7780 side order.MatchSide 7781 } 7782 7783 tests := []struct { 7784 name string 7785 orderQuantity uint64 7786 orderFilled uint64 7787 orderStatus order.OrderStatus 7788 rate uint64 7789 sell bool 7790 previousAccelerations []order.CoinID 7791 matches []testMatch 7792 expectRequiredForRemaining uint64 7793 expectError bool 7794 orderIDIncorrectLength bool 7795 nonActiveOrderID bool 7796 accelerateOrderError bool 7797 nilChangeCoin bool 7798 nilNewChangeCoin bool 7799 }{ 7800 { 7801 name: "ok", 7802 orderQuantity: 3 * dcrBtcLotSize, 7803 orderFilled: dcrBtcLotSize, 7804 previousAccelerations: []order.CoinID{encode.RandomBytes(32)}, 7805 orderStatus: order.OrderStatusExecuted, 7806 rate: dcrBtcRateStep * 10, 7807 expectRequiredForRemaining: 2*tMaxFeeRate*tSwapSizeB + calc.BaseToQuote(dcrBtcRateStep*10, 2*dcrBtcLotSize), 7808 matches: []testMatch{ 7809 { 7810 side: order.Maker, 7811 status: order.TakerSwapCast, 7812 quantity: dcrBtcLotSize, 7813 rate: dcrBtcRateStep * 10, 7814 }, 7815 }, 7816 }, 7817 { 7818 name: "ok - unswapped match, buy", 7819 orderQuantity: 8 * dcrBtcLotSize, 7820 orderFilled: 5 * dcrBtcLotSize, 7821 orderStatus: order.OrderStatusExecuted, 7822 previousAccelerations: []order.CoinID{encode.RandomBytes(32), encode.RandomBytes(32)}, 7823 rate: dcrBtcRateStep * 10, 7824 expectRequiredForRemaining: 4*tMaxFeeRate*tSwapSizeB + calc.BaseToQuote(dcrBtcRateStep*10, 5*dcrBtcLotSize), 7825 matches: []testMatch{ 7826 { 7827 side: order.Maker, 7828 status: order.TakerSwapCast, 7829 quantity: dcrBtcLotSize, 7830 rate: dcrBtcRateStep * 10, 7831 }, 7832 { 7833 side: order.Taker, 7834 status: order.TakerSwapCast, 7835 quantity: 2 * dcrBtcLotSize, 7836 rate: dcrBtcRateStep * 10, 7837 }, 7838 { 7839 side: order.Taker, 7840 status: order.MakerSwapCast, 7841 quantity: 2 * dcrBtcLotSize, 7842 rate: dcrBtcRateStep * 10, 7843 }, 7844 }, 7845 }, 7846 { 7847 name: "ok - unswapped match, sell", 7848 sell: true, 7849 previousAccelerations: []order.CoinID{encode.RandomBytes(32), encode.RandomBytes(32)}, 7850 orderQuantity: 8 * dcrBtcLotSize, 7851 orderFilled: 5 * dcrBtcLotSize, 7852 orderStatus: order.OrderStatusExecuted, 7853 rate: dcrBtcRateStep * 10, 7854 expectRequiredForRemaining: 4*tMaxFeeRate*tSwapSizeB + 5*dcrBtcLotSize, 7855 matches: []testMatch{ 7856 { 7857 side: order.Maker, 7858 status: order.TakerSwapCast, 7859 quantity: dcrBtcLotSize, 7860 rate: dcrBtcRateStep * 10, 7861 }, 7862 { 7863 side: order.Taker, 7864 status: order.TakerSwapCast, 7865 quantity: 2 * dcrBtcLotSize, 7866 rate: dcrBtcRateStep * 10, 7867 }, 7868 { 7869 side: order.Taker, 7870 status: order.MakerSwapCast, 7871 quantity: 2 * dcrBtcLotSize, 7872 rate: dcrBtcRateStep * 10, 7873 }, 7874 }, 7875 }, 7876 { 7877 name: "10 previous accelerations", 7878 sell: true, 7879 previousAccelerations: []order.CoinID{encode.RandomBytes(32), encode.RandomBytes(32), 7880 encode.RandomBytes(32), encode.RandomBytes(32), 7881 encode.RandomBytes(32), encode.RandomBytes(32), 7882 encode.RandomBytes(32), encode.RandomBytes(32), 7883 encode.RandomBytes(32), encode.RandomBytes(32)}, 7884 orderQuantity: 8 * dcrBtcLotSize, 7885 orderFilled: 5 * dcrBtcLotSize, 7886 orderStatus: order.OrderStatusExecuted, 7887 rate: dcrBtcRateStep * 10, 7888 matches: []testMatch{ 7889 { 7890 side: order.Maker, 7891 status: order.TakerSwapCast, 7892 quantity: dcrBtcLotSize, 7893 rate: dcrBtcRateStep * 10, 7894 }, 7895 }, 7896 expectError: true, 7897 }, 7898 { 7899 name: "no matches", 7900 orderQuantity: 3 * dcrBtcLotSize, 7901 orderFilled: dcrBtcLotSize, 7902 orderStatus: order.OrderStatusExecuted, 7903 rate: dcrBtcRateStep * 10, 7904 matches: []testMatch{}, 7905 expectError: true, 7906 }, 7907 { 7908 name: "no swap coins", 7909 orderQuantity: 3 * dcrBtcLotSize, 7910 orderFilled: dcrBtcLotSize, 7911 orderStatus: order.OrderStatusExecuted, 7912 rate: dcrBtcRateStep * 10, 7913 matches: []testMatch{{ 7914 side: order.Taker, 7915 status: order.MakerSwapCast, 7916 quantity: 2 * dcrBtcLotSize, 7917 rate: dcrBtcRateStep * 10, 7918 }}, 7919 expectError: true, 7920 }, 7921 { 7922 name: "incorrect length order id", 7923 orderQuantity: 3 * dcrBtcLotSize, 7924 orderFilled: dcrBtcLotSize, 7925 orderStatus: order.OrderStatusExecuted, 7926 rate: dcrBtcRateStep * 10, 7927 matches: []testMatch{ 7928 { 7929 side: order.Maker, 7930 status: order.TakerSwapCast, 7931 quantity: dcrBtcLotSize, 7932 rate: dcrBtcRateStep * 10, 7933 }, 7934 }, 7935 orderIDIncorrectLength: true, 7936 expectError: true, 7937 }, 7938 { 7939 name: "incorrect length order id", 7940 orderQuantity: 3 * dcrBtcLotSize, 7941 orderFilled: dcrBtcLotSize, 7942 orderStatus: order.OrderStatusExecuted, 7943 rate: dcrBtcRateStep * 10, 7944 matches: []testMatch{ 7945 { 7946 side: order.Maker, 7947 status: order.TakerSwapCast, 7948 quantity: dcrBtcLotSize, 7949 rate: dcrBtcRateStep * 10, 7950 }, 7951 }, 7952 nonActiveOrderID: true, 7953 expectError: true, 7954 }, 7955 { 7956 name: "accelerate order err", 7957 orderQuantity: 3 * dcrBtcLotSize, 7958 orderFilled: dcrBtcLotSize, 7959 orderStatus: order.OrderStatusExecuted, 7960 rate: dcrBtcRateStep * 10, 7961 matches: []testMatch{ 7962 { 7963 side: order.Maker, 7964 status: order.TakerSwapCast, 7965 quantity: dcrBtcLotSize, 7966 rate: dcrBtcRateStep * 10, 7967 }, 7968 }, 7969 accelerateOrderError: true, 7970 expectError: true, 7971 }, 7972 { 7973 name: "nil change coin", 7974 orderQuantity: 3 * dcrBtcLotSize, 7975 orderFilled: dcrBtcLotSize, 7976 orderStatus: order.OrderStatusExecuted, 7977 rate: dcrBtcRateStep * 10, 7978 matches: []testMatch{ 7979 { 7980 side: order.Maker, 7981 status: order.TakerSwapCast, 7982 quantity: dcrBtcLotSize, 7983 rate: dcrBtcRateStep * 10, 7984 }, 7985 }, 7986 nilChangeCoin: true, 7987 expectError: true, 7988 }, 7989 { 7990 name: "nil new change coin", 7991 orderQuantity: 3 * dcrBtcLotSize, 7992 orderFilled: dcrBtcLotSize, 7993 orderStatus: order.OrderStatusExecuted, 7994 rate: dcrBtcRateStep * 10, 7995 expectRequiredForRemaining: 2*tMaxFeeRate*tSwapSizeB + calc.BaseToQuote(dcrBtcRateStep*10, 2*dcrBtcLotSize), 7996 matches: []testMatch{ 7997 { 7998 side: order.Maker, 7999 status: order.TakerSwapCast, 8000 quantity: dcrBtcLotSize, 8001 rate: dcrBtcRateStep * 10, 8002 }, 8003 }, 8004 nilNewChangeCoin: true, 8005 }, 8006 } 8007 8008 for _, test := range tests { 8009 tBtcWallet.accelerateOrderErr = nil 8010 lo, dbOrder, preImg, addr := makeLimitOrder(dc, test.sell, test.orderQuantity, test.rate) 8011 dbOrder.MetaData.Status = test.orderStatus // so there is no order_status request for this 8012 oid := lo.ID() 8013 var walletSet *walletSet 8014 if test.sell { 8015 walletSet = sellWalletSet 8016 } else { 8017 walletSet = buyWalletSet 8018 } 8019 trade := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 8020 rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails) 8021 dc.trades[trade.ID()] = trade 8022 trade.Trade().AddFill(test.orderFilled) 8023 8024 trade.metaData.ChangeCoin = encode.RandomBytes(32) 8025 originalChangeCoin := trade.metaData.ChangeCoin 8026 trade.metaData.AccelerationCoins = test.previousAccelerations 8027 newChangeCoinID := dex.Bytes(encode.RandomBytes(32)) 8028 if test.nilNewChangeCoin { 8029 tBtcWallet.newChangeCoinID = nil 8030 } else { 8031 tBtcWallet.newChangeCoinID = &newChangeCoinID 8032 } 8033 tBtcWallet.newAccelerationTxID = hex.EncodeToString(encode.RandomBytes(32)) 8034 trade.matches = make(map[order.MatchID]*matchTracker) 8035 expectedSwapCoins := make([]order.CoinID, 0, len(test.matches)) 8036 for _, testMatch := range test.matches { 8037 matchID := ordertest.RandomMatchID() 8038 match := &matchTracker{ 8039 MetaMatch: db.MetaMatch{ 8040 MetaData: &db.MatchMetaData{ 8041 Proof: db.MatchProof{ 8042 MakerSwap: encode.RandomBytes(32), 8043 TakerSwap: encode.RandomBytes(32), 8044 }, 8045 }, 8046 UserMatch: &order.UserMatch{ 8047 MatchID: matchID, 8048 Address: addr, 8049 Side: testMatch.side, 8050 Status: testMatch.status, 8051 Quantity: testMatch.quantity, 8052 Rate: testMatch.rate, 8053 }, 8054 }, 8055 } 8056 if testMatch.side == order.Maker && testMatch.status >= order.MakerSwapCast { 8057 expectedSwapCoins = append(expectedSwapCoins, match.MetaData.Proof.MakerSwap) 8058 } 8059 if testMatch.side == order.Taker && testMatch.status >= order.TakerSwapCast { 8060 expectedSwapCoins = append(expectedSwapCoins, match.MetaData.Proof.TakerSwap) 8061 } 8062 trade.matches[matchID] = match 8063 } 8064 orderIDBytes := oid.Bytes() 8065 if test.orderIDIncorrectLength { 8066 orderIDBytes = encode.RandomBytes(31) 8067 } 8068 if test.nonActiveOrderID { 8069 orderIDBytes = encode.RandomBytes(32) 8070 } 8071 if test.accelerateOrderError { 8072 tBtcWallet.accelerateOrderErr = errors.New("") 8073 } 8074 if test.nilChangeCoin { 8075 trade.metaData.ChangeCoin = nil 8076 } 8077 8078 checkCommonCallValues := func() { 8079 t.Helper() 8080 swapCoins := tBtcWallet.accelerationParams.swapCoins 8081 if len(swapCoins) != len(expectedSwapCoins) { 8082 t.Fatalf("expected %d swap coins but got %d", len(expectedSwapCoins), len(swapCoins)) 8083 } 8084 8085 sort.Slice(swapCoins, func(i, j int) bool { return bytes.Compare(swapCoins[i], swapCoins[j]) > 0 }) 8086 sort.Slice(expectedSwapCoins, func(i, j int) bool { return bytes.Compare(expectedSwapCoins[i], expectedSwapCoins[j]) > 0 }) 8087 8088 for i := range swapCoins { 8089 if !bytes.Equal(swapCoins[i], expectedSwapCoins[i]) { 8090 t.Fatalf("expected swap coins not the same as actual") 8091 } 8092 } 8093 8094 changeCoin := tBtcWallet.accelerationParams.changeCoin 8095 if !bytes.Equal(changeCoin, originalChangeCoin) { 8096 t.Fatalf("change coin not same as expected %x - %x", changeCoin, trade.metaData.ChangeCoin) 8097 } 8098 8099 accelerationCoins := tBtcWallet.accelerationParams.accelerationCoins 8100 if len(accelerationCoins) != len(test.previousAccelerations) { 8101 t.Fatalf("expected 1 acceleration tx but got %v", len(accelerationCoins)) 8102 } 8103 for i := range accelerationCoins { 8104 if !bytes.Equal(accelerationCoins[i], test.previousAccelerations[i]) { 8105 t.Fatalf("expected acceleration coin not the same as actual") 8106 } 8107 } 8108 } 8109 8110 checkRequiredForRemainingSwaps := func() { 8111 t.Helper() 8112 if tBtcWallet.accelerationParams.requiredForRemainingSwaps != test.expectRequiredForRemaining { 8113 t.Fatalf("expected requiredForRemainingSwaps %d, but got %d", test.expectRequiredForRemaining, 8114 tBtcWallet.accelerationParams.requiredForRemainingSwaps) 8115 } 8116 } 8117 8118 testAccelerateOrder := func() { 8119 newFeeRate := rand.Uint64() 8120 txID, err := tCore.AccelerateOrder(tPW, orderIDBytes, newFeeRate) 8121 if test.expectError { 8122 if err == nil { 8123 t.Fatalf("expected error, but did not get") 8124 } 8125 return 8126 } 8127 if err != nil { 8128 t.Fatalf("unexpected error: %v", err) 8129 } 8130 8131 checkCommonCallValues() 8132 checkRequiredForRemainingSwaps() 8133 8134 if test.nilNewChangeCoin { 8135 if tBtcWallet.newChangeCoinID != nil { 8136 t.Fatalf("expected coin on order to be nil, but got %x", tBtcWallet.newChangeCoinID) 8137 } 8138 } else { 8139 if !bytes.Equal(trade.metaData.ChangeCoin, *tBtcWallet.newChangeCoinID) { 8140 t.Fatalf("change coin on trade was not updated to return value from AccelerateOrder") 8141 } 8142 if !bytes.Equal(trade.metaData.AccelerationCoins[len(trade.metaData.AccelerationCoins)-1], *tBtcWallet.newChangeCoinID) { 8143 t.Fatalf("new acceleration transaction id was not added to the trade") 8144 } 8145 8146 var inCoinsList bool 8147 for _, coin := range trade.coins { 8148 if bytes.Equal(coin.ID(), *tBtcWallet.newChangeCoinID) { 8149 inCoinsList = true 8150 } 8151 } 8152 if !inCoinsList { 8153 t.Fatalf("new change coin must be added to the trade.coins slice") 8154 } 8155 } 8156 if txID != tBtcWallet.newAccelerationTxID { 8157 t.Fatalf("new acceleration transaction id was not returned from AccelerateOrder") 8158 } 8159 if newFeeRate != tBtcWallet.accelerationParams.newFeeRate { 8160 t.Fatalf("%s: expected new fee rate %d, but got %d", test.name, 8161 newFeeRate, tBtcWallet.accelerationParams.newFeeRate) 8162 } 8163 } 8164 8165 testPreAccelerate := func() { 8166 rig.ws.queueResponse(msgjson.FeeRateRoute, feeRateSource) 8167 tBtcWallet.preAccelerateSwapRate = rand.Uint64() 8168 tBtcWallet.preAccelerateSuggestedRange = asset.XYRange{ 8169 Start: asset.XYRangePoint{ 8170 Label: "startLabel", 8171 X: rand.Float64(), 8172 Y: rand.Float64(), 8173 }, 8174 End: asset.XYRangePoint{ 8175 Label: "endLabel", 8176 X: rand.Float64(), 8177 Y: rand.Float64(), 8178 }, 8179 XUnit: "x", 8180 YUnit: "y", 8181 } 8182 8183 preAccelerate, err := tCore.PreAccelerateOrder(orderIDBytes) 8184 if test.expectError { 8185 if err == nil { 8186 t.Fatalf("expected error, but did not get") 8187 } 8188 return 8189 } 8190 if err != nil { 8191 t.Fatalf("%s: unexpected error: %v", test.name, err) 8192 } 8193 8194 checkCommonCallValues() 8195 checkRequiredForRemainingSwaps() 8196 8197 if !test.sell && preAccelerate.SuggestedRate != newQuoteFeeRate { 8198 t.Fatalf("%s: expected fee suggestion to be %d, but got %d", 8199 test.name, newQuoteFeeRate, preAccelerate.SuggestedRate) 8200 } 8201 if test.sell && preAccelerate.SuggestedRate != newBaseFeeRate { 8202 t.Fatalf("%s: expected fee suggestion to be %d, but got %d", 8203 test.name, newBaseFeeRate, preAccelerate.SuggestedRate) 8204 } 8205 if preAccelerate.SwapRate != tBtcWallet.preAccelerateSwapRate { 8206 t.Fatalf("%s: expected pre accelerate swap rate %d, but got %d", 8207 test.name, tBtcWallet.preAccelerateSwapRate, preAccelerate.SwapRate) 8208 } 8209 if !reflect.DeepEqual(preAccelerate.SuggestedRange, 8210 tBtcWallet.preAccelerateSuggestedRange) { 8211 t.Fatalf("%s: PreAccelerate suggested range not same as expected", 8212 test.name) 8213 } 8214 } 8215 8216 testMaxAcceleration := func() { 8217 t.Helper() 8218 tBtcWallet.accelerationEstimate = rand.Uint64() 8219 newFeeRate := rand.Uint64() 8220 estimate, err := tCore.AccelerationEstimate(orderIDBytes, newFeeRate) 8221 if test.expectError { 8222 if err == nil { 8223 t.Fatalf("expected error, but did not get") 8224 } 8225 return 8226 } 8227 if err != nil { 8228 t.Fatalf("%s: unexpected error: %v", test.name, err) 8229 } 8230 8231 checkCommonCallValues() 8232 checkRequiredForRemainingSwaps() 8233 8234 if newFeeRate != tBtcWallet.accelerationParams.newFeeRate { 8235 t.Fatalf("%s: expected new fee rate %d, but got %d", test.name, 8236 newFeeRate, tBtcWallet.accelerationParams.newFeeRate) 8237 } 8238 if estimate != tBtcWallet.accelerationEstimate { 8239 t.Fatalf("%s: expected acceleration estimate %d, but got %d", 8240 test.name, tBtcWallet.accelerationEstimate, estimate) 8241 } 8242 } 8243 8244 testPreAccelerate() 8245 testMaxAcceleration() 8246 testAccelerateOrder() 8247 } 8248 } 8249 8250 func TestMatchStatusResolution(t *testing.T) { 8251 rig := newTestRig() 8252 defer rig.shutdown() 8253 tCore := rig.core 8254 dc := rig.dc 8255 8256 dcrWallet, _ := newTWallet(tUTXOAssetA.ID) 8257 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 8258 btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) 8259 tCore.wallets[tUTXOAssetB.ID] = btcWallet 8260 walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true) 8261 8262 qty := 3 * dcrBtcLotSize 8263 secret := encode.RandomBytes(32) 8264 secretHash := sha256.Sum256(secret) 8265 8266 lo, dbOrder, preImg, addr := makeLimitOrder(dc, true, qty, dcrBtcRateStep*10) 8267 dbOrder.MetaData.Status = order.OrderStatusExecuted // so there is no order_status request for this 8268 oid := lo.ID() 8269 trade := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 8270 rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails) 8271 8272 dc.trades[trade.ID()] = trade 8273 matchID := ordertest.RandomMatchID() 8274 matchTime := time.Now() 8275 match := &matchTracker{ 8276 MetaMatch: db.MetaMatch{ 8277 MetaData: &db.MatchMetaData{}, 8278 UserMatch: &order.UserMatch{ 8279 MatchID: matchID, 8280 Address: addr, 8281 }, 8282 }, 8283 } 8284 trade.matches[matchID] = match 8285 8286 // oid order.OrderID, mid order.MatchID, recipient string, val uint64, secretHash []byte 8287 _, auditInfo := tMsgAudit(oid, matchID, addr, qty, secretHash[:]) 8288 tBtcWallet.auditInfo = auditInfo 8289 8290 connectMatches := func(status order.MatchStatus) []*msgjson.Match { 8291 return []*msgjson.Match{ 8292 { 8293 OrderID: oid[:], 8294 MatchID: matchID[:], 8295 Status: uint8(status), 8296 Side: uint8(match.Side), 8297 }, 8298 } 8299 8300 } 8301 8302 tBytes := encode.RandomBytes(2) 8303 tCoinID := encode.RandomBytes(36) 8304 tTxData := encode.RandomBytes(1) 8305 8306 setAuthSigs := func(status order.MatchStatus) { 8307 isMaker := match.Side == order.Maker 8308 match.MetaData.Proof.Auth = db.MatchAuth{} 8309 auth := &match.MetaData.Proof.Auth 8310 auth.MatchStamp = uint64(matchTime.UnixMilli()) 8311 if status >= order.MakerSwapCast { 8312 if isMaker { 8313 auth.InitSig = tBytes 8314 } else { 8315 auth.AuditSig = tBytes 8316 } 8317 } 8318 if status >= order.TakerSwapCast { 8319 if isMaker { 8320 auth.AuditSig = tBytes 8321 } else { 8322 auth.InitSig = tBytes 8323 } 8324 } 8325 if status >= order.MakerRedeemed { 8326 if isMaker { 8327 auth.RedeemSig = tBytes 8328 } else { 8329 auth.RedemptionSig = tBytes 8330 } 8331 } 8332 if status >= order.MatchComplete { 8333 if isMaker { 8334 auth.RedemptionSig = tBytes 8335 } else { 8336 auth.RedeemSig = tBytes 8337 } 8338 } 8339 } 8340 8341 // Call setProof before setAuthSigs 8342 setProof := func(status order.MatchStatus) { 8343 isMaker := match.Side == order.Maker 8344 match.Status = status 8345 match.MetaData.Proof = db.MatchProof{} 8346 proof := &match.MetaData.Proof 8347 8348 if isMaker { 8349 auditInfo.Expiration = matchTime.Add(trade.lockTimeTaker) 8350 } else { 8351 auditInfo.Expiration = matchTime.Add(trade.lockTimeMaker) 8352 } 8353 8354 if status >= order.MakerSwapCast { 8355 proof.MakerSwap = tCoinID 8356 proof.SecretHash = secretHash[:] 8357 if isMaker { 8358 proof.ContractData = tBytes 8359 proof.Secret = secret 8360 } else { 8361 proof.CounterContract = tBytes 8362 } 8363 } 8364 if status >= order.TakerSwapCast { 8365 proof.TakerSwap = tCoinID 8366 if isMaker { 8367 proof.CounterContract = tBytes 8368 } else { 8369 proof.ContractData = tBytes 8370 } 8371 } 8372 if status >= order.MakerRedeemed { 8373 proof.MakerRedeem = tCoinID 8374 if !isMaker { 8375 proof.Secret = secret 8376 } 8377 } 8378 if status >= order.MatchComplete { 8379 proof.TakerRedeem = tCoinID 8380 } 8381 } 8382 8383 setLocalMatchStatus := func(proofStatus, authStatus order.MatchStatus) { 8384 setProof(proofStatus) 8385 setAuthSigs(authStatus) 8386 } 8387 8388 var tMatchResults *msgjson.MatchStatusResult 8389 setMatchResults := func(status order.MatchStatus) *msgjson.MatchStatusResult { 8390 tMatchResults = &msgjson.MatchStatusResult{ 8391 MatchID: matchID[:], 8392 Status: uint8(status), 8393 Active: status != order.MatchComplete, 8394 } 8395 if status >= order.MakerSwapCast { 8396 tMatchResults.MakerContract = tBytes 8397 tMatchResults.MakerSwap = tCoinID 8398 } 8399 if status == order.MakerSwapCast || status == order.TakerSwapCast { 8400 tMatchResults.TakerTxData = tTxData 8401 } 8402 if status >= order.TakerSwapCast { 8403 tMatchResults.TakerContract = tBytes 8404 tMatchResults.TakerSwap = tCoinID 8405 } 8406 if status >= order.MakerRedeemed { 8407 tMatchResults.MakerRedeem = tCoinID 8408 tMatchResults.Secret = secret 8409 } 8410 if status >= order.MatchComplete { 8411 tMatchResults.TakerRedeem = tCoinID 8412 } 8413 return tMatchResults 8414 } 8415 8416 type test struct { 8417 ours, servers order.MatchStatus 8418 side order.MatchSide 8419 tweaker func() 8420 countStatusUpdates int 8421 } 8422 8423 testName := func(tt *test) string { 8424 return fmt.Sprintf("%s / %s (%s)", tt.ours, tt.servers, tt.side) 8425 } 8426 8427 runTest := func(tt *test) order.MatchStatus { 8428 match.Side = tt.side 8429 setLocalMatchStatus(tt.ours, tt.servers) 8430 setMatchResults(tt.servers) 8431 if tt.tweaker != nil { 8432 tt.tweaker() 8433 } 8434 rig.queueConnect(nil, connectMatches(tt.servers), nil) 8435 rig.ws.queueResponse(msgjson.MatchStatusRoute, func(msg *msgjson.Message, f msgFunc) error { 8436 resp, _ := msgjson.NewResponse(msg.ID, []*msgjson.MatchStatusResult{tMatchResults}, nil) 8437 f(resp) 8438 return nil 8439 }) 8440 if tt.countStatusUpdates > 0 { 8441 rig.db.updateMatchChan = make(chan order.MatchStatus, tt.countStatusUpdates) 8442 } 8443 if err := tCore.authDEX(dc); err != nil { 8444 t.Fatalf("unexpected authDEX error: %v", err) 8445 } 8446 for i := 0; i < tt.countStatusUpdates; i++ { 8447 <-rig.db.updateMatchChan 8448 } 8449 rig.db.updateMatchChan = nil 8450 trade.mtx.Lock() 8451 newStatus := match.Status 8452 trade.mtx.Unlock() 8453 return newStatus 8454 } 8455 8456 // forwardResolvers are recoverable status combos where the server is ahead 8457 // of us. 8458 forwardResolvers := []*test{ 8459 { 8460 ours: order.NewlyMatched, 8461 servers: order.MakerSwapCast, 8462 side: order.Taker, 8463 countStatusUpdates: 2, 8464 }, 8465 { 8466 ours: order.MakerSwapCast, 8467 servers: order.TakerSwapCast, 8468 side: order.Maker, 8469 countStatusUpdates: 2, 8470 }, 8471 { 8472 ours: order.TakerSwapCast, 8473 servers: order.MakerRedeemed, 8474 side: order.Taker, 8475 }, 8476 { 8477 ours: order.MakerRedeemed, 8478 servers: order.MatchComplete, 8479 side: order.Maker, 8480 }, 8481 } 8482 8483 // Check that all of the forwardResolvers update the match status. 8484 for _, tt := range forwardResolvers { 8485 newStatus := runTest(tt) 8486 if newStatus == tt.ours { 8487 t.Fatalf("(%s) status not updated for forward resolution path", testName(tt)) 8488 } 8489 if match.MetaData.Proof.SelfRevoked { 8490 t.Fatalf("(%s) match self-revoked during forward resolution", testName(tt)) 8491 } 8492 } 8493 8494 // backwardsResolvers are recoverable status mismatches where we are ahead 8495 // of the server but can be resolved by deferring to resendPendingRequests. 8496 backWardsResolvers := []*test{ 8497 { 8498 ours: order.MakerSwapCast, 8499 servers: order.NewlyMatched, 8500 side: order.Maker, 8501 }, 8502 { 8503 ours: order.TakerSwapCast, 8504 servers: order.MakerSwapCast, 8505 side: order.Taker, 8506 }, 8507 { 8508 ours: order.MakerRedeemed, 8509 servers: order.TakerSwapCast, 8510 side: order.Maker, 8511 }, 8512 { 8513 ours: order.MatchComplete, 8514 servers: order.MakerRedeemed, 8515 side: order.Taker, 8516 }, 8517 } 8518 8519 // Backwards resolvers won't update the match status, but also won't revoke 8520 // the match. 8521 for _, tt := range backWardsResolvers { 8522 newStatus := runTest(tt) 8523 if newStatus != tt.ours { 8524 t.Fatalf("(%s) status changed for backwards resolution path", testName(tt)) 8525 } 8526 if match.MetaData.Proof.SelfRevoked { 8527 t.Fatalf("(%s) match self-revoked during backwards resolution", testName(tt)) 8528 } 8529 } 8530 8531 // nonsense are status combos that make no sense, so should always result 8532 // in a self-revocation. 8533 nonsense := []*test{ 8534 { // Server has our info before us 8535 ours: order.NewlyMatched, 8536 servers: order.MakerSwapCast, 8537 side: order.Maker, 8538 }, 8539 { // Two steps apart 8540 ours: order.NewlyMatched, 8541 servers: order.TakerSwapCast, 8542 side: order.Maker, 8543 }, 8544 { // Server didn't send contract 8545 ours: order.NewlyMatched, 8546 servers: order.MakerSwapCast, 8547 side: order.Taker, 8548 tweaker: func() { 8549 tMatchResults.MakerContract = nil 8550 }, 8551 }, 8552 { // Server didn't send coin ID. 8553 ours: order.NewlyMatched, 8554 servers: order.MakerSwapCast, 8555 side: order.Taker, 8556 tweaker: func() { 8557 tMatchResults.MakerSwap = nil 8558 }, 8559 }, 8560 { // Audit failed. 8561 ours: order.NewlyMatched, 8562 servers: order.MakerSwapCast, 8563 side: order.Taker, 8564 tweaker: func() { 8565 auditInfo.Expiration = matchTime 8566 }, 8567 countStatusUpdates: 2, // async auditContract -> revoke and db update 8568 }, 8569 { // Server has our info before us 8570 ours: order.MakerSwapCast, 8571 servers: order.TakerSwapCast, 8572 side: order.Taker, 8573 }, 8574 { // Server has our info before us 8575 ours: order.MakerSwapCast, 8576 servers: order.TakerSwapCast, 8577 side: order.Taker, 8578 }, 8579 { // Server didn't send contract 8580 ours: order.MakerSwapCast, 8581 servers: order.TakerSwapCast, 8582 side: order.Maker, 8583 tweaker: func() { 8584 tMatchResults.TakerContract = nil 8585 }, 8586 }, 8587 { // Server didn't send coin ID. 8588 ours: order.MakerSwapCast, 8589 servers: order.TakerSwapCast, 8590 side: order.Maker, 8591 tweaker: func() { 8592 tMatchResults.TakerSwap = nil 8593 }, 8594 }, 8595 { // Audit failed. 8596 ours: order.MakerSwapCast, 8597 servers: order.TakerSwapCast, 8598 side: order.Maker, 8599 tweaker: func() { 8600 auditInfo.Expiration = matchTime 8601 }, 8602 countStatusUpdates: 2, // async auditContract -> revoke and db update 8603 }, 8604 { // Taker has counter-party info the server doesn't. 8605 ours: order.MakerSwapCast, 8606 servers: order.NewlyMatched, 8607 side: order.Taker, 8608 }, 8609 { // Maker has a server ack, but they say they don't have the data. 8610 ours: order.MakerSwapCast, 8611 servers: order.NewlyMatched, 8612 side: order.Maker, 8613 tweaker: func() { 8614 match.MetaData.Proof.Auth.InitSig = tBytes 8615 }, 8616 }, 8617 { // Maker has counter-party info the server doesn't. 8618 ours: order.TakerSwapCast, 8619 servers: order.MakerSwapCast, 8620 side: order.Maker, 8621 }, 8622 { // Taker has a server ack, but they say they don't have the data. 8623 ours: order.TakerSwapCast, 8624 servers: order.MakerSwapCast, 8625 side: order.Taker, 8626 tweaker: func() { 8627 match.MetaData.Proof.Auth.InitSig = tBytes 8628 }, 8629 }, 8630 { // Server has redeem info before us. 8631 ours: order.TakerSwapCast, 8632 servers: order.MakerRedeemed, 8633 side: order.Maker, 8634 }, 8635 { // Server didn't provide redemption coin ID. 8636 ours: order.TakerSwapCast, 8637 servers: order.MakerRedeemed, 8638 side: order.Taker, 8639 tweaker: func() { 8640 tMatchResults.MakerRedeem = nil 8641 }, 8642 }, 8643 { // Server didn't provide secret. 8644 ours: order.TakerSwapCast, 8645 servers: order.MakerRedeemed, 8646 side: order.Taker, 8647 tweaker: func() { 8648 tMatchResults.Secret = nil 8649 }, 8650 }, 8651 { // Server has our redemption data before us. 8652 ours: order.MakerRedeemed, 8653 servers: order.MatchComplete, 8654 side: order.Taker, 8655 }, 8656 { // We have data before the server. 8657 ours: order.MakerRedeemed, 8658 servers: order.TakerSwapCast, 8659 side: order.Taker, 8660 }, 8661 { // We have a server ack, but they say they don't have the data. 8662 ours: order.MakerRedeemed, 8663 servers: order.TakerSwapCast, 8664 side: order.Maker, 8665 tweaker: func() { 8666 match.MetaData.Proof.Auth.RedeemSig = tBytes 8667 }, 8668 }, 8669 { // We have data before the server. 8670 ours: order.MatchComplete, 8671 servers: order.MakerSwapCast, 8672 side: order.Maker, 8673 }, 8674 { // We have a server ack, but they say they don't have the data. 8675 ours: order.MatchComplete, 8676 servers: order.MakerSwapCast, 8677 side: order.Taker, 8678 tweaker: func() { 8679 match.MetaData.Proof.Auth.RedeemSig = tBytes 8680 }, 8681 }, 8682 } 8683 8684 for _, tt := range nonsense { 8685 runTest(tt) 8686 if !match.MetaData.Proof.SelfRevoked { 8687 t.Fatalf("(%s) match not self-revoked during nonsense resolution", testName(tt)) 8688 } 8689 } 8690 8691 // Run two matches for the same order. 8692 match2ID := ordertest.RandomMatchID() 8693 match2 := &matchTracker{ 8694 MetaMatch: db.MetaMatch{ 8695 MetaData: &db.MatchMetaData{}, 8696 UserMatch: &order.UserMatch{ 8697 MatchID: match2ID, 8698 Address: addr, 8699 }, 8700 }, 8701 } 8702 trade.matches[match2ID] = match2 8703 setAuthSigs(order.NewlyMatched) 8704 setProof(order.NewlyMatched) 8705 match2.Side = order.Taker 8706 match2.MetaData.Proof = match.MetaData.Proof 8707 8708 srvMatches := connectMatches(order.MakerSwapCast) 8709 srvMatches = append(srvMatches, &msgjson.Match{OrderID: oid[:], 8710 MatchID: match2ID[:], 8711 Status: uint8(order.MakerSwapCast), 8712 Side: uint8(order.Taker), 8713 }) 8714 8715 res1 := setMatchResults(order.MakerSwapCast) 8716 res2 := setMatchResults(order.MakerSwapCast) 8717 res2.MatchID = match2ID[:] 8718 8719 rig.queueConnect(nil, srvMatches, nil) 8720 rig.ws.queueResponse(msgjson.MatchStatusRoute, func(msg *msgjson.Message, f msgFunc) error { 8721 resp, _ := msgjson.NewResponse(msg.ID, []*msgjson.MatchStatusResult{res1, res2}, nil) 8722 f(resp) 8723 return nil 8724 }) 8725 // 2 matches resolved via contract audit: 2 synchronous updates, 2 async 8726 rig.db.updateMatchChan = make(chan order.MatchStatus, 4) 8727 tCore.authDEX(dc) 8728 for i := 0; i < 4; i++ { 8729 <-rig.db.updateMatchChan 8730 } 8731 trade.mtx.Lock() 8732 newStatus1 := match.Status 8733 newStatus2 := match2.Status 8734 trade.mtx.Unlock() 8735 if newStatus1 != order.MakerSwapCast { 8736 t.Fatalf("wrong status for match 1: %s", newStatus1) 8737 } 8738 if newStatus2 != order.MakerSwapCast { 8739 t.Fatalf("wrong status for match 2: %s", newStatus2) 8740 } 8741 } 8742 8743 func TestConfirmRedemption(t *testing.T) { 8744 rig := newTestRig() 8745 defer rig.shutdown() 8746 dc := rig.dc 8747 tCore := rig.core 8748 8749 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 8750 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 8751 btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) 8752 tCore.wallets[tUTXOAssetB.ID] = btcWallet 8753 walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true) 8754 8755 lo, dbOrder, preImg, addr := makeLimitOrder(dc, true, 0, 0) 8756 oid := lo.ID() 8757 tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 8758 rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails) 8759 dc.trades[oid] = tracker 8760 8761 tBytes := encode.RandomBytes(2) 8762 tCoinID := encode.RandomBytes(36) 8763 tUpdatedCoinID := encode.RandomBytes(36) 8764 secret := encode.RandomBytes(32) 8765 secretHash := sha256.Sum256(secret) 8766 8767 var match *matchTracker 8768 8769 tBtcWallet.redeemCoins = []dex.Bytes{tUpdatedCoinID} 8770 8771 ourContract := encode.RandomBytes(90) 8772 setupMatch := func(status order.MatchStatus, side order.MatchSide) { 8773 matchID := ordertest.RandomMatchID() 8774 _, auditInfo := tMsgAudit(oid, matchID, addr, 0, secretHash[:]) 8775 matchTime := time.Now() 8776 match = &matchTracker{ 8777 counterSwap: auditInfo, 8778 MetaMatch: db.MetaMatch{ 8779 MetaData: &db.MatchMetaData{}, 8780 UserMatch: &order.UserMatch{ 8781 MatchID: matchID, 8782 Address: addr, 8783 Side: side, 8784 Status: status, 8785 }, 8786 }, 8787 } 8788 tracker.matches = map[order.MatchID]*matchTracker{matchID: match} 8789 8790 isMaker := match.Side == order.Maker 8791 proof := &match.MetaData.Proof 8792 proof.Auth.InitSig = []byte{1, 2, 3, 4} 8793 // Assume our redeem was accepted, if we sent one. 8794 if isMaker { 8795 auditInfo.Expiration = matchTime.Add(tracker.lockTimeTaker) 8796 if status >= order.MakerRedeemed { 8797 match.MetaData.Proof.Auth.RedeemSig = []byte{0} 8798 } 8799 } else { 8800 auditInfo.Expiration = matchTime.Add(tracker.lockTimeMaker) 8801 if status >= order.MatchComplete { 8802 match.MetaData.Proof.Auth.RedeemSig = []byte{0} 8803 } 8804 } 8805 8806 if status >= order.MakerSwapCast { 8807 proof.MakerSwap = tCoinID 8808 proof.SecretHash = secretHash[:] 8809 if isMaker { 8810 proof.ContractData = ourContract 8811 proof.Secret = secret 8812 } else { 8813 proof.CounterContract = tBytes 8814 } 8815 } 8816 if status >= order.TakerSwapCast { 8817 proof.TakerSwap = tCoinID 8818 if isMaker { 8819 proof.CounterContract = tBytes 8820 } else { 8821 proof.ContractData = ourContract 8822 } 8823 } 8824 if status >= order.MakerRedeemed { 8825 proof.MakerRedeem = tCoinID 8826 if !isMaker { 8827 proof.Secret = secret 8828 } 8829 } 8830 if status >= order.MatchComplete { 8831 proof.TakerRedeem = tCoinID 8832 } 8833 } 8834 8835 type note struct { 8836 severity db.Severity 8837 topic db.Topic 8838 } 8839 8840 tests := []struct { 8841 name string 8842 matchStatus order.MatchStatus 8843 matchSide order.MatchSide 8844 expectedNotifications []*note 8845 confirmRedemptionResult *asset.ConfirmRedemptionStatus 8846 confirmRedemptionErr error 8847 8848 expectConfirmRedemptionCalled bool 8849 expectedStatus order.MatchStatus 8850 expectTicksDelayed bool 8851 }{ 8852 { 8853 name: "maker, makerRedeemed, confirmedRedemption", 8854 matchStatus: order.MakerRedeemed, 8855 matchSide: order.Maker, 8856 expectedNotifications: []*note{ 8857 { 8858 severity: db.Success, 8859 topic: TopicRedemptionConfirmed, 8860 }, 8861 }, 8862 confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ 8863 Confs: 10, 8864 Req: 10, 8865 CoinID: tCoinID, 8866 }, 8867 expectConfirmRedemptionCalled: true, 8868 expectedStatus: order.MatchConfirmed, 8869 }, 8870 { 8871 name: "maker, makerRedeemed, confirmedRedemption, more confs than required", 8872 matchStatus: order.MakerRedeemed, 8873 matchSide: order.Maker, 8874 expectedNotifications: []*note{ 8875 { 8876 severity: db.Success, 8877 topic: TopicRedemptionConfirmed, 8878 }, 8879 }, 8880 confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ 8881 Confs: 15, 8882 Req: 10, 8883 CoinID: tCoinID, 8884 }, 8885 expectConfirmRedemptionCalled: true, 8886 expectedStatus: order.MatchConfirmed, 8887 }, 8888 { 8889 name: "taker, matchComplete, confirmedRedemption", 8890 matchStatus: order.MatchComplete, 8891 matchSide: order.Taker, 8892 expectedNotifications: []*note{ 8893 { 8894 severity: db.Success, 8895 topic: TopicRedemptionConfirmed, 8896 }, 8897 }, 8898 confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ 8899 Confs: 10, 8900 Req: 10, 8901 CoinID: tCoinID, 8902 }, 8903 expectConfirmRedemptionCalled: true, 8904 expectedStatus: order.MatchConfirmed, 8905 }, 8906 { 8907 name: "maker, makerRedeemed, incomplete", 8908 matchStatus: order.MakerRedeemed, 8909 matchSide: order.Maker, 8910 expectedNotifications: []*note{ 8911 { 8912 severity: db.Data, 8913 topic: TopicConfirms, 8914 }, 8915 }, 8916 confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ 8917 Confs: 5, 8918 Req: 10, 8919 CoinID: tCoinID, 8920 }, 8921 expectConfirmRedemptionCalled: true, 8922 expectedStatus: order.MakerRedeemed, 8923 }, 8924 { 8925 name: "maker, makerRedeemed, replacedTx", 8926 matchStatus: order.MakerRedeemed, 8927 matchSide: order.Maker, 8928 expectedNotifications: []*note{ 8929 { 8930 severity: db.WarningLevel, 8931 topic: TopicRedemptionResubmitted, 8932 }, 8933 { 8934 severity: db.Data, 8935 topic: TopicConfirms, 8936 }, 8937 }, 8938 confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ 8939 Confs: 0, 8940 Req: 10, 8941 CoinID: tUpdatedCoinID, 8942 }, 8943 expectConfirmRedemptionCalled: true, 8944 expectedStatus: order.MakerRedeemed, 8945 }, 8946 { 8947 name: "taker, matchComplete, replacedTx", 8948 matchStatus: order.MatchComplete, 8949 matchSide: order.Taker, 8950 expectedNotifications: []*note{ 8951 { 8952 severity: db.WarningLevel, 8953 topic: TopicRedemptionResubmitted, 8954 }, 8955 { 8956 severity: db.Data, 8957 topic: TopicConfirms, 8958 }, 8959 }, 8960 confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ 8961 Confs: 0, 8962 Req: 10, 8963 CoinID: tUpdatedCoinID, 8964 }, 8965 expectConfirmRedemptionCalled: true, 8966 expectedStatus: order.MatchComplete, 8967 }, 8968 { 8969 // This case could happen if the dex was shut down right after 8970 // a resubmission. 8971 name: "taker, matchComplete, replacedTx and already confirmed", 8972 matchStatus: order.MatchComplete, 8973 matchSide: order.Taker, 8974 expectedNotifications: []*note{ 8975 { 8976 severity: db.WarningLevel, 8977 topic: TopicRedemptionResubmitted, 8978 }, 8979 { 8980 severity: db.Success, 8981 topic: TopicRedemptionConfirmed, 8982 }, 8983 }, 8984 confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ 8985 Confs: 10, 8986 Req: 10, 8987 CoinID: tUpdatedCoinID, 8988 }, 8989 expectConfirmRedemptionCalled: true, 8990 expectedStatus: order.MatchConfirmed, 8991 }, 8992 { 8993 name: "maker, makerRedeemed, error", 8994 matchStatus: order.MakerRedeemed, 8995 matchSide: order.Maker, 8996 confirmRedemptionErr: errors.New("err"), 8997 expectedStatus: order.MakerRedeemed, 8998 expectTicksDelayed: true, 8999 expectConfirmRedemptionCalled: true, 9000 }, 9001 { 9002 name: "maker, makerRedeemed, swap refunded error", 9003 matchStatus: order.MakerRedeemed, 9004 matchSide: order.Maker, 9005 confirmRedemptionErr: asset.ErrSwapRefunded, 9006 expectedStatus: order.MatchConfirmed, 9007 expectedNotifications: []*note{ 9008 { 9009 severity: db.ErrorLevel, 9010 topic: TopicSwapRefunded, 9011 }, 9012 }, 9013 expectConfirmRedemptionCalled: true, 9014 }, 9015 { 9016 name: "taker, takerRedeemed, redemption tx rejected error", 9017 matchStatus: order.MatchComplete, 9018 matchSide: order.Taker, 9019 confirmRedemptionErr: asset.ErrTxRejected, 9020 expectedStatus: order.MatchComplete, 9021 expectedNotifications: []*note{ 9022 { 9023 severity: db.Data, 9024 topic: TopicRedeemRejected, 9025 }, 9026 }, 9027 expectConfirmRedemptionCalled: true, 9028 }, 9029 { 9030 name: "maker, makerRedeemed, redemption tx lost", 9031 matchStatus: order.MakerRedeemed, 9032 matchSide: order.Maker, 9033 confirmRedemptionErr: asset.ErrTxLost, 9034 expectedStatus: order.TakerSwapCast, 9035 expectConfirmRedemptionCalled: true, 9036 }, 9037 { 9038 name: "taker, takerRedeemed, redemption tx lost", 9039 matchStatus: order.MatchComplete, 9040 matchSide: order.Taker, 9041 confirmRedemptionErr: asset.ErrTxLost, 9042 expectedStatus: order.MakerRedeemed, 9043 expectConfirmRedemptionCalled: true, 9044 }, 9045 { 9046 name: "maker, matchConfirmed", 9047 matchStatus: order.MatchConfirmed, 9048 matchSide: order.Maker, 9049 expectedStatus: order.MatchConfirmed, 9050 expectedNotifications: []*note{}, 9051 expectConfirmRedemptionCalled: false, 9052 }, 9053 { 9054 name: "maker, TakerSwapCast", 9055 matchStatus: order.TakerSwapCast, 9056 matchSide: order.Maker, 9057 expectedStatus: order.TakerSwapCast, 9058 expectedNotifications: []*note{}, 9059 expectConfirmRedemptionCalled: false, 9060 }, 9061 { 9062 name: "taker, TakerSwapCast", 9063 matchStatus: order.TakerSwapCast, 9064 matchSide: order.Taker, 9065 expectedStatus: order.TakerSwapCast, 9066 expectedNotifications: []*note{}, 9067 expectConfirmRedemptionCalled: false, 9068 }, 9069 } 9070 9071 notificationFeed := tCore.NotificationFeed() 9072 9073 for _, test := range tests { 9074 tracker.mtx.Lock() 9075 setupMatch(test.matchStatus, test.matchSide) 9076 tracker.mtx.Unlock() 9077 9078 tBtcWallet.confirmRedemptionResult = test.confirmRedemptionResult 9079 tBtcWallet.confirmRedemptionErr = test.confirmRedemptionErr 9080 tBtcWallet.confirmRedemptionCalled = false 9081 9082 tCore.tickAsset(dc, tUTXOAssetB.ID) 9083 9084 if tBtcWallet.confirmRedemptionCalled != test.expectConfirmRedemptionCalled { 9085 t.Fatalf("%s: expected confirm redemption to be called=%v but got=%v", 9086 test.name, test.expectConfirmRedemptionCalled, tBtcWallet.confirmRedemptionCalled) 9087 } 9088 9089 for _, expectedNotification := range test.expectedNotifications { 9090 var n Notification 9091 out: 9092 for { 9093 select { 9094 case n = <-notificationFeed.C: 9095 if n.Topic() == expectedNotification.topic { 9096 break out 9097 } 9098 case <-time.After(60 * time.Second): 9099 t.Fatalf("%s: did not receive expected notification", test.name) 9100 } 9101 } 9102 9103 if n.Severity() != expectedNotification.severity { 9104 t.Fatalf("%s: expected severity %v, got %v", 9105 test.name, expectedNotification.severity, n.Severity()) 9106 } 9107 } 9108 9109 tracker.mtx.RLock() 9110 if test.confirmRedemptionResult != nil { 9111 var redeemCoin order.CoinID 9112 if test.matchSide == order.Maker { 9113 redeemCoin = match.MetaData.Proof.MakerRedeem 9114 } else { 9115 redeemCoin = match.MetaData.Proof.TakerRedeem 9116 } 9117 if !bytes.Equal(redeemCoin, test.confirmRedemptionResult.CoinID) { 9118 t.Fatalf("%s: expected coin %v != actual %v", test.name, test.confirmRedemptionResult.CoinID, redeemCoin) 9119 } 9120 if test.confirmRedemptionResult.Confs >= test.confirmRedemptionResult.Req { 9121 if len(tDcrWallet.returnedContracts) != 1 || !bytes.Equal(ourContract, tDcrWallet.returnedContracts[0]) { 9122 t.Fatalf("%s: refund address not returned", test.name) 9123 } 9124 } 9125 } 9126 9127 ticksDelayed := match.tickGovernor != nil 9128 if ticksDelayed != test.expectTicksDelayed { 9129 t.Fatalf("%s: expected ticks delayed %v but got %v", test.name, test.expectTicksDelayed, ticksDelayed) 9130 } 9131 9132 if match.Status != test.expectedStatus { 9133 t.Fatalf("%s: expected status %v but got %v", test.name, test.expectedStatus, match.Status) 9134 } 9135 tracker.mtx.RUnlock() 9136 } 9137 } 9138 9139 func TestMaxSwapsRedeemsInTx(t *testing.T) { 9140 rig := newTestRig() 9141 defer rig.shutdown() 9142 dc := rig.dc 9143 tCore := rig.core 9144 9145 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 9146 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 9147 btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) 9148 tCore.wallets[tUTXOAssetB.ID] = btcWallet 9149 walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true) 9150 9151 tDcrWallet.info.MaxSwapsInTx = 4 9152 tBtcWallet.info.MaxRedeemsInTx = 4 9153 9154 lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, 0, 0) 9155 oid := lo.ID() 9156 tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 9157 rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails) 9158 dc.trades[oid] = tracker 9159 9160 newMatch := func(side order.MatchSide, status order.MatchStatus) *matchTracker { 9161 return &matchTracker{ 9162 prefix: lo.Prefix(), 9163 trade: lo.Trade(), 9164 MetaMatch: db.MetaMatch{ 9165 MetaData: &db.MatchMetaData{ 9166 Proof: db.MatchProof{ 9167 Auth: db.MatchAuth{ 9168 MatchStamp: uint64(time.Now().UnixMilli()), 9169 AuditStamp: uint64(time.Now().UnixMilli()), 9170 }, 9171 }, 9172 }, 9173 UserMatch: &order.UserMatch{ 9174 MatchID: ordertest.RandomMatchID(), 9175 Side: side, 9176 Address: ordertest.RandomAddress(), 9177 Status: status, 9178 FeeRateSwap: tMaxFeeRate, 9179 }, 9180 }, 9181 } 9182 } 9183 9184 swapabbleMatches := func(num int) map[order.MatchID]*matchTracker { 9185 matches := make(map[order.MatchID]*matchTracker, num) 9186 for i := 0; i < num; i++ { 9187 m := newMatch(order.Maker, order.NewlyMatched) 9188 matches[m.MatchID] = m 9189 } 9190 return matches 9191 } 9192 9193 redeemableMatches := func(num int) map[order.MatchID]*matchTracker { 9194 matches := make(map[order.MatchID]*matchTracker, num) 9195 for i := 0; i < num; i++ { 9196 m := newMatch(order.Taker, order.MakerRedeemed) 9197 matches[m.MatchID] = m 9198 rig.ws.queueResponse(msgjson.RedeemRoute, redeemAcker) 9199 } 9200 return matches 9201 } 9202 9203 checkNumSwaps := func(expected []int, wallet *TXCWallet) { 9204 t.Helper() 9205 for i := range expected { 9206 if expected[i] != len(wallet.lastSwaps[i].Contracts) { 9207 t.Fatalf("expected %d swaps but got %d", expected[i], len(wallet.lastSwaps[i].Contracts)) 9208 } 9209 } 9210 } 9211 9212 checkNumRedeems := func(expected []int, wallet *TXCWallet) { 9213 t.Helper() 9214 for i := range expected { 9215 if expected[i] != len(wallet.lastRedeems[i].Redemptions) { 9216 t.Fatalf("expected %d swaps but got %d", expected[i], len(wallet.lastRedeems[i].Redemptions)) 9217 } 9218 } 9219 } 9220 9221 populateRedeemCoins := func(num int, wallet *TXCWallet) { 9222 wallet.redeemCoins = make([]dex.Bytes, num) 9223 for i := 0; i < num; i++ { 9224 wallet.redeemCoins = append(wallet.redeemCoins, encode.RandomBytes(32)) 9225 } 9226 } 9227 9228 // Test Swaps 9229 expected := []int{4, 4, 4, 4, 4, 2} 9230 tracker.matches = swapabbleMatches(22) 9231 tCore.tick(tracker) 9232 checkNumSwaps(expected, tDcrWallet) 9233 9234 tDcrWallet.lastSwaps = make([]*asset.Swaps, 0) 9235 expected = []int{3} 9236 tracker.matches = swapabbleMatches(3) 9237 tCore.tick(tracker) 9238 checkNumSwaps(expected, tDcrWallet) 9239 9240 tDcrWallet.lastSwaps = make([]*asset.Swaps, 0) 9241 expected = []int{4} 9242 tracker.matches = swapabbleMatches(4) 9243 tCore.tick(tracker) 9244 checkNumSwaps(expected, tDcrWallet) 9245 9246 // Test Redeems 9247 expected = []int{4, 4, 4, 4, 4, 2} 9248 tracker.matches = redeemableMatches(22) 9249 populateRedeemCoins(22, tBtcWallet) 9250 tCore.tick(tracker) 9251 checkNumRedeems(expected, tBtcWallet) 9252 9253 tBtcWallet.lastRedeems = make([]*asset.RedeemForm, 0) 9254 expected = []int{3} 9255 tracker.matches = redeemableMatches(3) 9256 populateRedeemCoins(3, tBtcWallet) 9257 tCore.tick(tracker) 9258 checkNumRedeems(expected, tBtcWallet) 9259 9260 tBtcWallet.lastRedeems = make([]*asset.RedeemForm, 0) 9261 expected = []int{4} 9262 tracker.matches = redeemableMatches(4) 9263 populateRedeemCoins(4, tBtcWallet) 9264 tCore.tick(tracker) 9265 checkNumRedeems(expected, tBtcWallet) 9266 } 9267 9268 func TestSuspectTrades(t *testing.T) { 9269 rig := newTestRig() 9270 defer rig.shutdown() 9271 dc := rig.dc 9272 tCore := rig.core 9273 9274 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 9275 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 9276 btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) 9277 tCore.wallets[tUTXOAssetB.ID] = btcWallet 9278 walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true) 9279 9280 lo, dbOrder, preImg, addr := makeLimitOrder(dc, true, 0, 0) 9281 oid := lo.ID() 9282 tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker, 9283 rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails) 9284 dc.trades[oid] = tracker 9285 9286 newMatch := func(side order.MatchSide, status order.MatchStatus) *matchTracker { 9287 return &matchTracker{ 9288 prefix: lo.Prefix(), 9289 trade: lo.Trade(), 9290 MetaMatch: db.MetaMatch{ 9291 MetaData: &db.MatchMetaData{ 9292 Proof: db.MatchProof{ 9293 Auth: db.MatchAuth{ 9294 MatchStamp: uint64(time.Now().UnixMilli()), 9295 AuditStamp: uint64(time.Now().UnixMilli()), 9296 }, 9297 }, 9298 }, 9299 UserMatch: &order.UserMatch{ 9300 MatchID: ordertest.RandomMatchID(), 9301 Side: side, 9302 Address: ordertest.RandomAddress(), 9303 Status: status, 9304 FeeRateSwap: tMaxFeeRate, 9305 }, 9306 }, 9307 } 9308 } 9309 9310 var swappableMatch1, swappableMatch2 *matchTracker 9311 setSwaps := func() { 9312 swappableMatch1 = newMatch(order.Maker, order.NewlyMatched) 9313 swappableMatch2 = newMatch(order.Taker, order.MakerSwapCast) 9314 9315 // Set counterswaps for both swaps. 9316 // Set valid wallet auditInfo for swappableMatch2, taker will repeat audit before swapping. 9317 auditQty := calc.BaseToQuote(swappableMatch2.Rate, swappableMatch2.Quantity) 9318 _, auditInfo := tMsgAudit(oid, swappableMatch2.MatchID, addr, auditQty, encode.RandomBytes(32)) 9319 auditInfo.Expiration = encode.DropMilliseconds(swappableMatch2.matchTime().Add(tracker.lockTimeMaker)) 9320 tBtcWallet.setConfs(auditInfo.Coin.ID(), tUTXOAssetA.SwapConf, nil) 9321 tBtcWallet.auditInfo = auditInfo 9322 swappableMatch2.counterSwap = auditInfo 9323 9324 _, auditInfo = tMsgAudit(oid, swappableMatch1.MatchID, ordertest.RandomAddress(), 1, encode.RandomBytes(32)) 9325 tBtcWallet.setConfs(auditInfo.Coin.ID(), tUTXOAssetA.SwapConf, nil) 9326 swappableMatch1.counterSwap = auditInfo 9327 9328 tDcrWallet.swapCounter = 0 9329 tracker.matches = map[order.MatchID]*matchTracker{ 9330 swappableMatch1.MatchID: swappableMatch1, 9331 swappableMatch2.MatchID: swappableMatch2, 9332 } 9333 } 9334 setSwaps() 9335 9336 // Initial success 9337 _, err := tCore.tick(tracker) 9338 if err != nil { 9339 t.Fatalf("swap tick error: %v", err) 9340 } 9341 9342 setSwaps() 9343 tDcrWallet.swapErr = tErr 9344 _, err = tCore.tick(tracker) 9345 if err == nil || !strings.Contains(err.Error(), "error sending dcr swap transaction") { 9346 t.Fatalf("swap error not propagated, err = %v", err) 9347 } 9348 if tDcrWallet.swapCounter != 1 { 9349 t.Fatalf("never swapped") 9350 } 9351 9352 // Both matches should be marked as suspect and have tickGovernors in place. 9353 tracker.mtx.Lock() 9354 for i, m := range []*matchTracker{swappableMatch1, swappableMatch2} { 9355 if !m.suspectSwap { 9356 t.Fatalf("swappable match %d not suspect after failed swap", i+1) 9357 } 9358 if m.tickGovernor == nil { 9359 t.Fatalf("swappable match %d has no tick meterer set", i+1) 9360 } 9361 } 9362 tracker.mtx.Unlock() 9363 9364 // Ticking right away again should do nothing. 9365 tDcrWallet.swapErr = nil 9366 _, err = tCore.tick(tracker) 9367 if err != nil { 9368 t.Fatalf("tick error during metered swap tick: %v", err) 9369 } 9370 if tDcrWallet.swapCounter != 1 { 9371 t.Fatalf("swapped during metered tick") 9372 } 9373 9374 // But once the tickGovernors expire, we should succeed with two separate 9375 // requests. 9376 tracker.mtx.Lock() 9377 swappableMatch1.tickGovernor = nil 9378 swappableMatch2.tickGovernor = nil 9379 tracker.mtx.Unlock() 9380 _, err = tCore.tick(tracker) 9381 if err != nil { 9382 t.Fatalf("tick error while swapping suspect matches: %v", err) 9383 } 9384 if tDcrWallet.swapCounter != 3 { 9385 t.Fatalf("suspect swap matches not run or not run separately. expected 2 new calls to Swap, got %d", tDcrWallet.swapCounter-1) 9386 } 9387 9388 var redeemableMatch1, redeemableMatch2 *matchTracker 9389 setRedeems := func() { 9390 redeemableMatch1 = newMatch(order.Maker, order.TakerSwapCast) 9391 redeemableMatch2 = newMatch(order.Taker, order.MakerRedeemed) 9392 9393 // Set valid wallet auditInfo for redeemableMatch1, maker will repeat audit before redeeming. 9394 auditQty := calc.BaseToQuote(redeemableMatch1.Rate, redeemableMatch1.Quantity) 9395 _, auditInfo := tMsgAudit(oid, redeemableMatch1.MatchID, addr, auditQty, encode.RandomBytes(32)) 9396 auditInfo.Expiration = encode.DropMilliseconds(redeemableMatch1.matchTime().Add(tracker.lockTimeTaker)) 9397 tBtcWallet.setConfs(auditInfo.Coin.ID(), tUTXOAssetB.SwapConf, nil) 9398 tBtcWallet.auditInfo = auditInfo 9399 redeemableMatch1.counterSwap = auditInfo 9400 redeemableMatch1.MetaData.Proof.SecretHash = auditInfo.SecretHash 9401 9402 tBtcWallet.redeemCounter = 0 9403 tracker.matches = map[order.MatchID]*matchTracker{ 9404 redeemableMatch1.MatchID: redeemableMatch1, 9405 redeemableMatch2.MatchID: redeemableMatch2, 9406 } 9407 rig.ws.queueResponse(msgjson.RedeemRoute, redeemAcker) 9408 rig.ws.queueResponse(msgjson.RedeemRoute, redeemAcker) 9409 } 9410 setRedeems() 9411 9412 // Initial success 9413 tBtcWallet.redeemCoins = []dex.Bytes{encode.RandomBytes(36), encode.RandomBytes(36)} 9414 _, err = tCore.tick(tracker) 9415 if err != nil { 9416 t.Fatalf("redeem tick error: %v", err) 9417 } 9418 if tBtcWallet.redeemCounter != 1 { 9419 t.Fatalf("never redeemed") 9420 } 9421 9422 setRedeems() 9423 tBtcWallet.redeemErr = tErr 9424 _, err = tCore.tick(tracker) 9425 if err == nil || !strings.Contains(err.Error(), "error sending redeem transaction") { 9426 t.Fatalf("redeem error not propagated. err = %v", err) 9427 } 9428 if tBtcWallet.redeemCounter != 1 { 9429 t.Fatalf("never redeemed") 9430 } 9431 9432 // Both matches should be marked as suspect and have tickGovernors in place. 9433 tracker.mtx.Lock() 9434 for i, m := range []*matchTracker{redeemableMatch1, redeemableMatch2} { 9435 if !m.suspectRedeem { 9436 t.Fatalf("redeemable match %d not suspect after failed swap", i+1) 9437 } 9438 if m.tickGovernor == nil { 9439 t.Fatalf("redeemable match %d has no tick meterer set", i+1) 9440 } 9441 } 9442 tracker.mtx.Unlock() 9443 9444 // Ticking right away again should do nothing. 9445 tBtcWallet.redeemErr = nil 9446 _, err = tCore.tick(tracker) 9447 if err != nil { 9448 t.Fatalf("tick error during metered redeem tick: %v", err) 9449 } 9450 if tBtcWallet.redeemCounter != 1 { 9451 t.Fatalf("redeemed during metered tick %d", tBtcWallet.redeemCounter) 9452 } 9453 9454 // But once the tickGovernors expire, we should succeed with two separate 9455 // requests. 9456 tracker.mtx.Lock() 9457 redeemableMatch1.tickGovernor = nil 9458 redeemableMatch2.tickGovernor = nil 9459 tracker.mtx.Unlock() 9460 _, err = tCore.tick(tracker) 9461 if err != nil { 9462 t.Fatalf("tick error while redeeming suspect matches: %v", err) 9463 } 9464 if tBtcWallet.redeemCounter != 3 { 9465 t.Fatalf("suspect redeem matches not run or not run separately. expected 2 new calls to Redeem, got %d", tBtcWallet.redeemCounter-1) 9466 } 9467 } 9468 9469 func TestWalletSyncing(t *testing.T) { 9470 rig := newTestRig() 9471 defer rig.shutdown() 9472 tCore := rig.core 9473 9474 noteFeed := tCore.NotificationFeed() 9475 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 9476 dcrWallet.syncStatus.Synced = false 9477 dcrWallet.syncStatus.Blocks = 0 9478 dcrWallet.hookedUp = false 9479 // Connect with tCore.connectWallet below. 9480 9481 tStart := time.Now() 9482 testDuration := 100 * time.Millisecond 9483 syncTickerPeriod = 10 * time.Millisecond 9484 9485 tDcrWallet.syncStatus = func() (bool, float32, error) { 9486 progress := float32(float64(time.Since(tStart)) / float64(testDuration)) 9487 if progress >= 1 { 9488 return true, 1, nil 9489 } 9490 return false, progress, nil 9491 } 9492 9493 _, err := tCore.connectWallet(dcrWallet) 9494 if err != nil { 9495 t.Fatalf("connectWallet error: %v", err) 9496 } 9497 9498 timeout := time.NewTimer(time.Second) 9499 defer timeout.Stop() 9500 var progressNotes int 9501 out: 9502 for { 9503 select { 9504 case note := <-noteFeed.C: 9505 syncNote, ok := note.(*WalletSyncNote) 9506 if !ok { 9507 continue 9508 } 9509 progressNotes++ 9510 if syncNote.SyncStatus.Synced { 9511 break out 9512 } 9513 case <-timeout.C: 9514 t.Fatalf("timed out waiting for synced wallet note. Received %d progress notes", progressNotes) 9515 } 9516 } 9517 // By the time we've got 10th note it should signal that the wallet has been 9518 // synced (due to how we've set up testDuration and syncTickerPeriod values). 9519 if progressNotes > 10 { 9520 t.Fatalf("expected 10 progress notes at most, got %d", progressNotes) 9521 } 9522 } 9523 9524 func TestParseCert(t *testing.T) { 9525 byteCert := []byte{0x0a, 0x0b} 9526 cert, err := parseCert("anyhost", []byte{0x0a, 0x0b}, dex.Mainnet) 9527 if err != nil { 9528 t.Fatalf("byte cert error: %v", err) 9529 } 9530 if !bytes.Equal(cert, byteCert) { 9531 t.Fatalf("byte cert note returned unmodified. expected %x, got %x", byteCert, cert) 9532 } 9533 byteCert = []byte{0x05, 0x06} 9534 certFile, _ := os.CreateTemp("", "dumbcert") 9535 defer os.Remove(certFile.Name()) 9536 certFile.Write(byteCert) 9537 certFile.Close() 9538 cert, err = parseCert("anyhost", certFile.Name(), dex.Mainnet) 9539 if err != nil { 9540 t.Fatalf("file cert error: %v", err) 9541 } 9542 if !bytes.Equal(cert, byteCert) { 9543 t.Fatalf("byte cert note returned unmodified. expected %x, got %x", byteCert, cert) 9544 } 9545 _, err = parseCert("bison.exchange:17232", []byte(nil), dex.Testnet) 9546 if err != nil { 9547 t.Fatalf("CertStore cert error: %v", err) 9548 } 9549 } 9550 9551 func TestPreOrder(t *testing.T) { 9552 rig := newTestRig() 9553 defer rig.shutdown() 9554 tCore := rig.core 9555 dc := rig.dc 9556 9557 btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID) 9558 tCore.wallets[tUTXOAssetB.ID] = btcWallet 9559 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 9560 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 9561 9562 var rate uint64 = 1e8 9563 quoteConvertedLotSize := calc.BaseToQuote(rate, dcrBtcLotSize) 9564 9565 book := newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger) 9566 dc.books[tDcrBtcMktName] = book 9567 9568 sellNote := &msgjson.BookOrderNote{ 9569 OrderNote: msgjson.OrderNote{ 9570 OrderID: encode.RandomBytes(32), 9571 }, 9572 TradeNote: msgjson.TradeNote{ 9573 Side: msgjson.SellOrderNum, 9574 Quantity: quoteConvertedLotSize * 10, 9575 Time: uint64(time.Now().Unix()), 9576 Rate: rate, 9577 }, 9578 } 9579 9580 buyNote := *sellNote 9581 buyNote.TradeNote.Quantity = dcrBtcLotSize * 10 9582 buyNote.TradeNote.Side = msgjson.BuyOrderNum 9583 9584 var baseFeeRate uint64 = 5 9585 var quoteFeeRate uint64 = 10 9586 9587 err := book.Sync(&msgjson.OrderBook{ 9588 MarketID: tDcrBtcMktName, 9589 Seq: 1, 9590 Epoch: 1, 9591 Orders: []*msgjson.BookOrderNote{sellNote, &buyNote}, 9592 BaseFeeRate: baseFeeRate, 9593 QuoteFeeRate: quoteFeeRate, 9594 }) 9595 if err != nil { 9596 t.Fatalf("Sync error: %v", err) 9597 } 9598 9599 preSwap := &asset.PreSwap{ 9600 Estimate: &asset.SwapEstimate{ 9601 MaxFees: 1001, 9602 Lots: 5, 9603 RealisticBestCase: 15, 9604 RealisticWorstCase: 20, 9605 }, 9606 } 9607 9608 tBtcWallet.preSwap = preSwap 9609 9610 preRedeem := &asset.PreRedeem{ 9611 Estimate: &asset.RedeemEstimate{ 9612 RealisticBestCase: 15, 9613 RealisticWorstCase: 20, 9614 }, 9615 } 9616 9617 tDcrWallet.preRedeem = preRedeem 9618 9619 form := &TradeForm{ 9620 Host: tDexHost, 9621 Sell: false, 9622 // IsLimit: true, 9623 Base: tUTXOAssetA.ID, 9624 Quote: tUTXOAssetB.ID, 9625 Qty: quoteConvertedLotSize * 5, 9626 Rate: rate, 9627 } 9628 preOrder, err := tCore.PreOrder(form) 9629 if err != nil { 9630 t.Fatalf("PreOrder market buy error: %v", err) 9631 } 9632 9633 compUint64 := func(tag string, a, b uint64) { 9634 t.Helper() 9635 if a != b { 9636 t.Fatalf("%s: %d != %d", tag, a, b) 9637 } 9638 } 9639 9640 est1, est2 := preSwap.Estimate, preOrder.Swap.Estimate 9641 compUint64("MaxFees", est1.MaxFees, est2.MaxFees) 9642 compUint64("RealisticWorstCase", est1.RealisticWorstCase, est2.RealisticWorstCase) 9643 compUint64("RealisticBestCase", est1.RealisticBestCase, est2.RealisticBestCase) 9644 // This is a buy order, so the from asset is the quote asset. 9645 compUint64("PreOrder.FeeSuggestion.quote", quoteFeeRate, tBtcWallet.preSwapForm.FeeSuggestion) 9646 compUint64("PreOrder.FeeSuggestion.base", baseFeeRate, tDcrWallet.preRedeemForm.FeeSuggestion) 9647 9648 // Missing book is an error 9649 delete(dc.books, tDcrBtcMktName) 9650 _, err = tCore.PreOrder(form) 9651 if err == nil { 9652 t.Fatalf("no error for market order with missing book") 9653 } 9654 dc.books[tDcrBtcMktName] = book 9655 9656 // Exercise the market sell path too. 9657 form.Sell = true 9658 _, err = tCore.PreOrder(form) 9659 if err != nil { 9660 t.Fatalf("PreOrder market sell error: %v", err) 9661 } 9662 9663 // Market orders have to have a market to make estimates. 9664 book.Unbook(&msgjson.UnbookOrderNote{ 9665 MarketID: tDcrBtcMktName, 9666 OrderID: sellNote.OrderID, 9667 }) 9668 book.Unbook(&msgjson.UnbookOrderNote{ 9669 MarketID: tDcrBtcMktName, 9670 OrderID: buyNote.OrderID, 9671 }) 9672 _, err = tCore.PreOrder(form) 9673 if err == nil { 9674 t.Fatalf("no error for market order with empty market") 9675 } 9676 9677 // Limit orders have no such restriction. 9678 form.IsLimit = true 9679 _, err = tCore.PreOrder(form) 9680 if err != nil { 9681 t.Fatalf("PreOrder limit sell error: %v", err) 9682 } 9683 9684 var newBaseFeeRate uint64 = 55 9685 var newQuoteFeeRate uint64 = 65 9686 feeRateSource := func(msg *msgjson.Message, f msgFunc) error { 9687 var resp *msgjson.Message 9688 if string(msg.Payload) == "42" { 9689 resp, _ = msgjson.NewResponse(msg.ID, newBaseFeeRate, nil) 9690 } else { 9691 resp, _ = msgjson.NewResponse(msg.ID, newQuoteFeeRate, nil) 9692 } 9693 f(resp) 9694 return nil 9695 } 9696 9697 // Removing the book should cause us to 9698 delete(dc.books, tDcrBtcMktName) 9699 rig.ws.queueResponse(msgjson.FeeRateRoute, feeRateSource) 9700 rig.ws.queueResponse(msgjson.FeeRateRoute, feeRateSource) 9701 9702 _, err = tCore.PreOrder(form) 9703 if err != nil { 9704 t.Fatalf("PreOrder limit sell error #2: %v", err) 9705 } 9706 // sell order now, so from asset is base asset 9707 compUint64("PreOrder.FeeSuggestion quote asset from server", newQuoteFeeRate, tBtcWallet.preRedeemForm.FeeSuggestion) 9708 compUint64("PreOrder.FeeSuggestion base asset from server", newBaseFeeRate, tDcrWallet.preSwapForm.FeeSuggestion) 9709 dc.books[tDcrBtcMktName] = book 9710 9711 // no DEX 9712 delete(tCore.conns, dc.acct.host) 9713 _, err = tCore.PreOrder(form) 9714 if err == nil { 9715 t.Fatalf("no error for unknown DEX") 9716 } 9717 tCore.conns[dc.acct.host] = dc 9718 9719 // no wallet 9720 delete(tCore.wallets, tUTXOAssetA.ID) 9721 _, err = tCore.PreOrder(form) 9722 if err == nil { 9723 t.Fatalf("no error for missing wallet") 9724 } 9725 tCore.wallets[tUTXOAssetA.ID] = dcrWallet 9726 9727 // base wallet not connected 9728 dcrWallet.hookedUp = false 9729 tDcrWallet.connectErr = tErr 9730 _, err = tCore.PreOrder(form) 9731 if err == nil { 9732 t.Fatalf("no error for unconnected base wallet") 9733 } 9734 dcrWallet.hookedUp = true 9735 tDcrWallet.connectErr = nil 9736 9737 // quote wallet not connected 9738 btcWallet.hookedUp = false 9739 tBtcWallet.connectErr = tErr 9740 _, err = tCore.PreOrder(form) 9741 if err == nil { 9742 t.Fatalf("no error for unconnected quote wallet") 9743 } 9744 btcWallet.hookedUp = true 9745 tBtcWallet.connectErr = nil 9746 9747 // success again 9748 _, err = tCore.PreOrder(form) 9749 if err != nil { 9750 t.Fatalf("PreOrder error after fixing everything: %v", err) 9751 } 9752 } 9753 9754 func TestRefreshServerConfig(t *testing.T) { 9755 rig := newTestRig() 9756 defer rig.shutdown() 9757 9758 // Add an API version to supportedAPIVers to use in tests. 9759 const newAPIVer = ^uint16(0) - 1 9760 supportedAPIVers = append(supportedAPIVers, int32(newAPIVer)) 9761 9762 queueConfig := func(err *msgjson.Error, apiVer uint16) { 9763 rig.ws.queueResponse(msgjson.ConfigRoute, func(msg *msgjson.Message, f msgFunc) error { 9764 cfg := *rig.dc.cfg 9765 cfg.APIVersion = apiVer 9766 resp, _ := msgjson.NewResponse(msg.ID, cfg, err) 9767 f(resp) 9768 return nil 9769 }) 9770 } 9771 tests := []struct { 9772 name string 9773 configErr *msgjson.Error 9774 gotAPIVer uint16 9775 marketBase uint32 9776 wantErr bool 9777 }{{ 9778 name: "ok", 9779 marketBase: tUTXOAssetA.ID, 9780 gotAPIVer: newAPIVer, 9781 }, { 9782 name: "unable to fetch config", 9783 configErr: new(msgjson.Error), 9784 wantErr: true, 9785 }, { 9786 name: "api not in wanted versions", 9787 gotAPIVer: ^uint16(0), 9788 marketBase: tUTXOAssetA.ID, 9789 wantErr: true, 9790 }, { 9791 name: "generate maps failure", 9792 marketBase: ^uint32(0), 9793 gotAPIVer: newAPIVer, 9794 wantErr: true, 9795 }} 9796 9797 for _, test := range tests { 9798 rig.dc.cfg.Markets[0].Base = test.marketBase 9799 queueConfig(test.configErr, test.gotAPIVer) 9800 _, err := rig.dc.refreshServerConfig() 9801 if test.wantErr { 9802 if err == nil { 9803 t.Fatalf("expected error for test %q", test.name) 9804 } 9805 continue 9806 } 9807 if err != nil { 9808 t.Fatalf("unexpected error for test %q: %v", test.name, err) 9809 } 9810 } 9811 } 9812 9813 func TestCredentialHandling(t *testing.T) { 9814 rig := newTestRig() 9815 defer rig.shutdown() 9816 tCore := rig.core 9817 9818 clearCreds := func() { 9819 tCore.credentials = nil 9820 rig.db.creds = nil 9821 } 9822 9823 clearCreds() 9824 tCore.newCrypter = encrypt.NewCrypter 9825 tCore.reCrypter = encrypt.Deserialize 9826 9827 _, err := tCore.InitializeClient(tPW, nil) 9828 if err != nil { 9829 t.Fatalf("InitializeClient error: %v", err) 9830 } 9831 9832 // Since the actual encrypt package crypter is now used instead of the dummy 9833 // tCrypter, the acct.encKey should be updated to reflect acct.privKey. 9834 // Although the test does not rely on this, we should keep the dexAccount 9835 // self-consistent and avoid confusing messages in the test log. 9836 err = rig.resetAcctEncKey(tPW) 9837 if err != nil { 9838 t.Fatalf("InitializeClient error: %v", err) 9839 } 9840 9841 tCore.Logout() 9842 9843 err = tCore.Login(tPW) 9844 if err != nil { 9845 t.Fatalf("Login error: %v", err) 9846 } 9847 // NOTE: a warning note is expected. "Wallet connection warning - Incomplete 9848 // registration detected for somedex.tld:7232, but failed to connect to the 9849 // Decred wallet" 9850 } 9851 9852 func TestCoreAssetSeedAndPass(t *testing.T) { 9853 // This test ensures the derived wallet seed and password are deterministic 9854 // and depend on both asset ID and app seed. 9855 9856 // NOTE: the blake256 hash of an empty slice is: 9857 // []byte{0x71, 0x6f, 0x6e, 0x86, 0x3f, 0x74, 0x4b, 0x9a, 0xc2, 0x2c, 0x97, 0xec, 0x7b, 0x76, 0xea, 0x5f, 9858 // 0x59, 0x8, 0xbc, 0x5b, 0x2f, 0x67, 0xc6, 0x15, 0x10, 0xbf, 0xc4, 0x75, 0x13, 0x84, 0xea, 0x7a} 9859 // The above was very briefly the password for all seeded wallets, not released. 9860 9861 tests := []struct { 9862 name string 9863 appSeed []byte 9864 assetID uint32 9865 wantSeed []byte 9866 wantPass []byte 9867 }{ 9868 { 9869 name: "base", 9870 appSeed: []byte{1, 2, 3}, 9871 assetID: 2, 9872 wantSeed: []byte{ 9873 0xac, 0x61, 0xb1, 0xbc, 0x77, 0xd0, 0xa6, 0xd5, 0xd2, 0xb5, 0xc9, 0x77, 0x91, 0xd6, 0x4a, 0xaf, 9874 0x4a, 0xa3, 0x47, 0xb7, 0xb, 0x85, 0xe, 0x82, 0x1c, 0x79, 0xab, 0xc0, 0x86, 0x50, 0xee, 0xda}, 9875 wantPass: []byte{ 9876 0xd8, 0xf0, 0x27, 0x4d, 0xbc, 0x56, 0xb0, 0x74, 0x1e, 0x20, 0x3b, 0x98, 0xe9, 0xaa, 0x5c, 0xba, 9877 0x13, 0xfd, 0x60, 0x3b, 0x83, 0x76, 0x2e, 0x4b, 0x5d, 0x6d, 0x19, 0x57, 0x89, 0xe2, 0x8b, 0xc7}, 9878 }, 9879 { 9880 name: "change app seed", 9881 appSeed: []byte{2, 2, 3}, 9882 assetID: 2, 9883 wantSeed: []byte{ 9884 0xf, 0xc9, 0xf, 0xa8, 0xb3, 0xe9, 0x31, 0x2a, 0xba, 0xf1, 0xda, 0x70, 0x41, 0x81, 0x49, 0xed, 9885 0xad, 0x47, 0x9, 0xcd, 0xe2, 0x17, 0x14, 0xd, 0x63, 0x49, 0x8a, 0xd8, 0xff, 0x1f, 0x3e, 0x8b}, 9886 wantPass: []byte{ 9887 0x78, 0x21, 0x72, 0x59, 0xbe, 0x39, 0xea, 0x54, 0x10, 0x46, 0x7d, 0x7e, 0xa, 0x95, 0xc4, 0xa0, 9888 0xd8, 0x73, 0xce, 0x1, 0xb2, 0x49, 0x98, 0x6c, 0x68, 0xc5, 0x69, 0x69, 0xa7, 0x13, 0xc1, 0xce}, 9889 }, 9890 { 9891 name: "change asset ID", 9892 appSeed: []byte{1, 2, 3}, 9893 assetID: 0, 9894 wantSeed: []byte{ 9895 0xe1, 0xad, 0x62, 0xe4, 0x60, 0xfd, 0x75, 0x91, 0x3d, 0x41, 0x2e, 0x8e, 0xc5, 0x72, 0xd4, 0xa2, 9896 0x39, 0x2d, 0x32, 0x86, 0xf0, 0x6b, 0xf7, 0xdf, 0x48, 0xcc, 0x57, 0xb1, 0x4b, 0x7b, 0xc6, 0xce}, 9897 wantPass: []byte{ 9898 0x52, 0xba, 0x59, 0x21, 0xd3, 0xc5, 0x6b, 0x2, 0x2c, 0x12, 0xc1, 0x98, 0xdc, 0x84, 0xed, 0x68, 9899 0x6, 0x35, 0xa6, 0x25, 0xd0, 0xc4, 0x49, 0x5a, 0x13, 0xc3, 0x12, 0xfb, 0xeb, 0xb3, 0x61, 0x88}, 9900 }, 9901 } 9902 for _, tt := range tests { 9903 t.Run(tt.name, func(t *testing.T) { 9904 seed, pass := AssetSeedAndPass(tt.assetID, tt.appSeed) 9905 if !bytes.Equal(pass, tt.wantPass) { 9906 t.Errorf("pass not as expected, got %#v", pass) 9907 } 9908 if !bytes.Equal(seed, tt.wantSeed) { 9909 t.Errorf("seed not as expected, got %#v", seed) 9910 } 9911 }) 9912 } 9913 } 9914 9915 var randU32 = func() uint32 { return uint32(rand.Int31()) } 9916 9917 func randOrderForMarket(base, quote uint32) order.Order { 9918 switch rand.Intn(3) { 9919 case 0: 9920 o, _ := ordertest.RandomCancelOrder() 9921 o.BaseAsset = base 9922 o.QuoteAsset = quote 9923 return o 9924 case 1: 9925 o, _ := ordertest.RandomMarketOrder() 9926 o.BaseAsset = base 9927 o.QuoteAsset = quote 9928 return o 9929 default: 9930 o, _ := ordertest.RandomLimitOrder() 9931 o.BaseAsset = base 9932 o.QuoteAsset = quote 9933 return o 9934 } 9935 } 9936 9937 func randBytes(n int) []byte { 9938 b := make([]byte, n) 9939 rand.Read(b) 9940 return b 9941 } 9942 9943 func TestDeleteOrderFn(t *testing.T) { 9944 rig := newTestRig() 9945 defer rig.shutdown() 9946 tCore := rig.core 9947 9948 randomOdrs := func() []*db.MetaOrder { 9949 acct1 := dbtest.RandomAccountInfo() 9950 acct2 := dbtest.RandomAccountInfo() 9951 base1, quote1 := tUTXOAssetA.ID, tUTXOAssetB.ID 9952 base2, quote2 := tACCTAsset.ID, tUTXOAssetA.ID 9953 n := rand.Intn(9) + 1 9954 orders := make([]*db.MetaOrder, n) 9955 for i := 0; i < n; i++ { 9956 acct := acct1 9957 base, quote := base1, quote1 9958 if i%2 == 1 { 9959 acct = acct2 9960 base, quote = base2, quote2 9961 } 9962 ord := randOrderForMarket(base, quote) 9963 orders[i] = &db.MetaOrder{ 9964 MetaData: &db.OrderMetaData{ 9965 Status: order.OrderStatus(rand.Intn(5) + 1), 9966 Host: acct.Host, 9967 Proof: db.OrderProof{DEXSig: randBytes(73)}, 9968 SwapFeesPaid: rand.Uint64(), 9969 RedemptionFeesPaid: rand.Uint64(), 9970 MaxFeeRate: rand.Uint64(), 9971 }, 9972 Order: ord, 9973 } 9974 } 9975 return orders 9976 } 9977 9978 ordersFile, err := os.CreateTemp("", "delete_archives_test_orders") 9979 if err != nil { 9980 t.Fatal(err) 9981 } 9982 ordersFileName := ordersFile.Name() 9983 ordersFile.Close() 9984 os.Remove(ordersFileName) 9985 9986 tests := []struct { 9987 name, ordersFileStr string 9988 wantErr bool 9989 }{{ 9990 name: "ok orders and file save", 9991 ordersFileStr: ordersFileName, 9992 }, { 9993 name: "bad file (already closed)", 9994 ordersFileStr: ordersFileName, 9995 wantErr: true, 9996 }} 9997 9998 for _, test := range tests { 9999 perOrdFn, cleanupFn, err := tCore.deleteOrderFn(test.ordersFileStr) 10000 if test.wantErr { 10001 if err != nil { 10002 continue 10003 } 10004 t.Fatalf("%q: expected error", test.name) 10005 } 10006 if err != nil { 10007 t.Fatalf("%q: unexpected failure: %v", test.name, err) 10008 } 10009 for _, o := range randomOdrs() { 10010 err = perOrdFn(o) 10011 if err != nil { 10012 t.Fatalf("%q: unexpected failure: %v", test.name, err) 10013 } 10014 } 10015 cleanupFn() 10016 } 10017 10018 b, err := os.ReadFile(ordersFileName) 10019 if err != nil { 10020 t.Fatalf("unable to read file: %s", ordersFileName) 10021 } 10022 fmt.Println(string(b)) 10023 os.Remove(ordersFileName) 10024 } 10025 10026 func TestDeleteMatchFn(t *testing.T) { 10027 randomMtchs := func() []*db.MetaMatch { 10028 base, quote := tUTXOAssetA.ID, tUTXOAssetB.ID 10029 acct := dbtest.RandomAccountInfo() 10030 n := rand.Intn(9) + 1 10031 metaMatches := make([]*db.MetaMatch, 0, n) 10032 for i := 0; i < n; i++ { 10033 m := &db.MetaMatch{ 10034 MetaData: &db.MatchMetaData{ 10035 Proof: *dbtest.RandomMatchProof(0.5), 10036 DEX: acct.Host, 10037 Base: base, 10038 Quote: quote, 10039 Stamp: rand.Uint64(), 10040 }, 10041 UserMatch: ordertest.RandomUserMatch(), 10042 } 10043 if i%2 == 1 { 10044 m.Status = order.MatchStatus(rand.Intn(4)) 10045 } else { 10046 m.Status = order.MatchComplete // inactive 10047 m.MetaData.Proof.Auth.RedeemSig = []byte{0} // redeemSig required for MatchComplete to be considered inactive 10048 } 10049 metaMatches = append(metaMatches, m) 10050 } 10051 return metaMatches 10052 } 10053 10054 matchesFile, err := os.CreateTemp("", "delete_archives_test_matches") 10055 if err != nil { 10056 t.Fatal(err) 10057 } 10058 matchesFileName := matchesFile.Name() 10059 matchesFile.Close() 10060 os.Remove(matchesFileName) 10061 10062 tests := []struct { 10063 name, matchesFileStr string 10064 wantErr bool 10065 }{{ 10066 name: "ok matches and file save", 10067 matchesFileStr: matchesFileName, 10068 }, { 10069 name: "bad file (already closed)", 10070 matchesFileStr: matchesFileName, 10071 wantErr: true, 10072 }} 10073 10074 for _, test := range tests { 10075 perMatchFn, cleanupFn, err := deleteMatchFn(test.matchesFileStr) 10076 if test.wantErr { 10077 if err != nil { 10078 continue 10079 } 10080 t.Fatalf("%q: expected error", test.name) 10081 } 10082 if err != nil { 10083 t.Fatalf("%q: unexpected failure: %v", test.name, err) 10084 } 10085 for _, m := range randomMtchs() { 10086 err = perMatchFn(m, true) 10087 if err != nil { 10088 t.Fatalf("%q: unexpected failure: %v", test.name, err) 10089 } 10090 } 10091 cleanupFn() 10092 } 10093 10094 b, err := os.ReadFile(matchesFileName) 10095 if err != nil { 10096 t.Fatalf("unable to read file: %s", matchesFileName) 10097 } 10098 fmt.Println(string(b)) 10099 os.Remove(matchesFileName) 10100 } 10101 10102 func TestDeleteArchivedRecords(t *testing.T) { 10103 rig := newTestRig() 10104 defer rig.shutdown() 10105 tCore := rig.core 10106 tdb := tCore.db.(*TDB) 10107 10108 tempFile := func(suffix string) (path string) { 10109 matchesFile, err := os.CreateTemp("", suffix+"delete_archives_test_matches") 10110 if err != nil { 10111 t.Fatal(err) 10112 } 10113 matchesFileName := matchesFile.Name() 10114 matchesFile.Close() 10115 os.Remove(matchesFileName) 10116 return matchesFileName 10117 } 10118 10119 tests := []struct { 10120 name string 10121 olderThan *time.Time 10122 matchesFileStr, ordersFileStr string 10123 archivedMatches, archivedOrders int 10124 deleteInactiveOrdersErr, deleteInactiveMatchesErr error 10125 wantErr bool 10126 }{{ 10127 name: "ok no order or file save", 10128 archivedMatches: 12, 10129 archivedOrders: 24, 10130 }, { 10131 name: "ok orders and file save", 10132 ordersFileStr: tempFile("abc"), 10133 matchesFileStr: tempFile("123"), 10134 archivedMatches: 34, 10135 archivedOrders: 67, 10136 }, { 10137 name: "orders save error", 10138 ordersFileStr: tempFile("abc"), 10139 deleteInactiveOrdersErr: errors.New(""), 10140 wantErr: true, 10141 }, { 10142 name: "matches save error", 10143 matchesFileStr: tempFile("123"), 10144 deleteInactiveMatchesErr: errors.New(""), 10145 wantErr: true, 10146 }} 10147 10148 for _, test := range tests { 10149 tdb.archivedMatches = test.archivedMatches 10150 tdb.archivedOrders = test.archivedOrders 10151 tdb.deleteInactiveOrdersErr = test.deleteInactiveOrdersErr 10152 tdb.deleteInactiveMatchesErr = test.deleteInactiveMatchesErr 10153 nRecordsDeleted, err := tCore.DeleteArchivedRecords(test.olderThan, test.matchesFileStr, test.ordersFileStr) 10154 if test.wantErr { 10155 if err != nil { 10156 continue 10157 } 10158 t.Fatalf("%q: expected error", test.name) 10159 } 10160 if err != nil { 10161 t.Fatalf("%q: unexpected failure: %v", test.name, err) 10162 } 10163 expectedRecords := test.archivedMatches + test.archivedOrders 10164 if nRecordsDeleted != expectedRecords { 10165 t.Fatalf("%s: Expected %d deleted records, got %d", test.name, expectedRecords, nRecordsDeleted) 10166 } 10167 } 10168 } 10169 10170 func TestLCM(t *testing.T) { 10171 tests := []struct { 10172 name string 10173 a, b, wantDenom, wantMultA, wantMultB uint64 10174 }{{ 10175 name: "ok 5 and 10", 10176 a: 5, 10177 b: 10, 10178 wantDenom: 10, 10179 wantMultA: 2, 10180 wantMultB: 1, 10181 }, { 10182 name: "ok 3 and 7", 10183 a: 3, 10184 b: 7, 10185 wantDenom: 21, 10186 wantMultA: 7, 10187 wantMultB: 3, 10188 }, { 10189 name: "ok 6 and 34", 10190 a: 34, 10191 b: 6, 10192 wantDenom: 102, 10193 wantMultA: 3, 10194 wantMultB: 17, 10195 }} 10196 10197 for _, test := range tests { 10198 denom, multA, multB := lcm(test.a, test.b) 10199 if denom != test.wantDenom || multA != test.wantMultA || multB != test.wantMultB { 10200 t.Fatalf("%q: expected %d %d %d but got %d %d %d", test.name, 10201 test.wantDenom, test.wantMultA, test.wantMultB, denom, multA, multB) 10202 } 10203 } 10204 } 10205 10206 func TestToggleRateSourceStatus(t *testing.T) { 10207 rig := newTestRig() 10208 defer rig.shutdown() 10209 tCore := rig.core 10210 10211 tests := []struct { 10212 name, source string 10213 wantErr, init bool 10214 }{{ 10215 name: "Invalid rate source", 10216 source: "binance", 10217 wantErr: true, 10218 }, { 10219 name: "ok valid source", 10220 source: messari, 10221 wantErr: false, 10222 }, { 10223 name: "ok already disabled/not initialized || enabled", 10224 source: messari, 10225 wantErr: false, 10226 }} 10227 10228 // Test disabling fiat rate source. 10229 for _, test := range tests { 10230 err := tCore.ToggleRateSourceStatus(test.source, true) 10231 if test.wantErr != (err != nil) { 10232 t.Fatalf("%s: wantErr = %t, err = %v", test.name, test.wantErr, err) 10233 } 10234 } 10235 10236 // Test enabling fiat rate source. 10237 for _, test := range tests { 10238 if test.init { 10239 tCore.fiatRateSources[test.source] = newCommonRateSource(tFetcher) 10240 } 10241 err := tCore.ToggleRateSourceStatus(test.source, false) 10242 if test.wantErr != (err != nil) { 10243 t.Fatalf("%s: wantErr = %t, err = %v", test.name, test.wantErr, err) 10244 } 10245 } 10246 } 10247 10248 func TestFiatRateSources(t *testing.T) { 10249 rig := newTestRig() 10250 defer rig.shutdown() 10251 tCore := rig.core 10252 supportedFetchers := len(fiatRateFetchers) 10253 rateSources := tCore.FiatRateSources() 10254 if len(rateSources) != supportedFetchers { 10255 t.Fatalf("Expected %d number of fiat rate source/fetchers", supportedFetchers) 10256 } 10257 } 10258 10259 func TestFiatConversions(t *testing.T) { 10260 rig := newTestRig() 10261 defer rig.shutdown() 10262 tCore := rig.core 10263 10264 // No fiat rate source initialized 10265 fiatRates := tCore.fiatConversions() 10266 if len(fiatRates) != 0 { 10267 t.Fatal("Unexpected asset rate values.") 10268 } 10269 10270 // Initialize fiat rate sources. 10271 for token := range fiatRateFetchers { 10272 tCore.fiatRateSources[token] = newCommonRateSource(tFetcher) 10273 } 10274 10275 // Fetch fiat rates. 10276 tCore.wg.Add(1) 10277 go func() { 10278 defer tCore.wg.Done() 10279 tCore.refreshFiatRates(tCtx) 10280 }() 10281 tCore.wg.Wait() 10282 10283 // Expects assets fiat rate values. 10284 fiatRates = tCore.fiatConversions() 10285 if len(fiatRates) != 2 { 10286 t.Fatal("Expected assets fiat rate for two assets") 10287 } 10288 10289 // fiat rates for assets can expire, and fiat rate fetchers can be 10290 // removed if expired. 10291 for token, source := range tCore.fiatRateSources { 10292 source.fiatRates[tUTXOAssetA.ID].lastUpdate = time.Now().Add(-time.Minute) 10293 source.fiatRates[tUTXOAssetB.ID].lastUpdate = time.Now().Add(-time.Minute) 10294 if source.isExpired(55 * time.Second) { 10295 delete(tCore.fiatRateSources, token) 10296 } 10297 } 10298 10299 fiatRates = tCore.fiatConversions() 10300 if len(fiatRates) != 0 { 10301 t.Fatal("Unexpected assets fiat rate values, expected to ignore expired fiat rates.") 10302 } 10303 10304 if len(tCore.fiatRateSources) != 0 { 10305 t.Fatal("Expected fiat conversion to be disabled, all rate source data has expired.") 10306 } 10307 } 10308 10309 func TestValidateAddress(t *testing.T) { 10310 rig := newTestRig() 10311 defer rig.shutdown() 10312 tCore := rig.core 10313 10314 wallet, tWallet := newTWallet(tUTXOAssetA.ID) 10315 tCore.wallets[tUTXOAssetA.ID] = wallet 10316 10317 tests := []struct { 10318 name string 10319 addr string 10320 wantValidAddr bool 10321 wantMissingWallet bool 10322 wantErr bool 10323 }{{ 10324 name: "valid address", 10325 addr: "randomvalidaddress", 10326 wantValidAddr: true, 10327 }, { 10328 name: "invalid address", 10329 addr: "", 10330 }, { 10331 name: "wallet not found", 10332 addr: "randomaddr", 10333 wantMissingWallet: true, 10334 wantErr: true, 10335 }} 10336 for _, test := range tests { 10337 tWallet.validAddr = test.wantValidAddr 10338 if test.wantMissingWallet { 10339 tCore.wallets = make(map[uint32]*xcWallet) 10340 } 10341 valid, err := tCore.ValidateAddress(test.addr, tUTXOAssetA.ID) 10342 if test.wantErr { 10343 if err != nil { 10344 continue 10345 } 10346 t.Fatalf("%s: expected error", test.name) 10347 } 10348 if test.wantValidAddr != valid { 10349 t.Fatalf("Got wrong response for address validation, got %v expected %v", valid, test.wantValidAddr) 10350 } 10351 } 10352 } 10353 10354 func TestEstimateSendTxFee(t *testing.T) { 10355 rig := newTestRig() 10356 defer rig.shutdown() 10357 tCore := rig.core 10358 10359 tests := []struct { 10360 name string 10361 asset uint32 10362 estFee uint64 10363 value uint64 10364 subtract bool 10365 wantMissingWallet bool 10366 wantErr bool 10367 }{{ 10368 name: "ok", 10369 asset: tUTXOAssetA.ID, 10370 subtract: true, 10371 estFee: 1e8, 10372 value: 1e8, 10373 }, { 10374 name: "zero amount", 10375 asset: tACCTAsset.ID, 10376 subtract: true, 10377 wantErr: true, 10378 }, { 10379 name: "subtract true and not withdrawer", 10380 asset: tACCTAsset.ID, 10381 subtract: true, 10382 wantErr: true, 10383 value: 1e8, 10384 }, { 10385 name: "wallet not found", 10386 asset: tUTXOAssetA.ID, 10387 wantErr: true, 10388 wantMissingWallet: true, 10389 value: 1e8, 10390 }} 10391 10392 for _, test := range tests { 10393 wallet, tWallet := newTWallet(test.asset) 10394 tCore.wallets[test.asset] = wallet 10395 if test.wantMissingWallet { 10396 delete(tCore.wallets, test.asset) 10397 } 10398 10399 tWallet.estFee = test.estFee 10400 10401 tWallet.estFeeErr = nil 10402 if test.wantErr { 10403 tWallet.estFeeErr = tErr 10404 } 10405 estimate, _, err := tCore.EstimateSendTxFee("addr", test.asset, test.value, test.subtract, false) 10406 if test.wantErr { 10407 if err != nil { 10408 continue 10409 } 10410 t.Fatalf("%s: expected error", test.name) 10411 } 10412 if err != nil { 10413 t.Fatalf("%s: unexpected error: %v", test.name, err) 10414 } 10415 10416 if estimate != test.estFee { 10417 t.Fatalf("%s: expected fee %v, got %v", test.name, test.estFee, estimate) 10418 } 10419 if !test.wantErr && err != nil { 10420 t.Fatalf("%s: unexpected error", test.name) 10421 } 10422 } 10423 } 10424 10425 type TDynamicSwapper struct { 10426 *TXCWallet 10427 tfpPaid uint64 10428 tfpSecretHashes [][]byte 10429 tfpErr error 10430 } 10431 10432 func (dtfc *TDynamicSwapper) DynamicSwapFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (uint64, [][]byte, error) { 10433 return dtfc.tfpPaid, dtfc.tfpSecretHashes, dtfc.tfpErr 10434 } 10435 func (dtfc *TDynamicSwapper) DynamicRedemptionFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (uint64, [][]byte, error) { 10436 return dtfc.tfpPaid, dtfc.tfpSecretHashes, dtfc.tfpErr 10437 } 10438 10439 var _ asset.DynamicSwapper = (*TDynamicSwapper)(nil) 10440 10441 func TestUpdateFeesPaid(t *testing.T) { 10442 ctx, cancel := context.WithCancel(context.Background()) 10443 defer cancel() 10444 tests := []struct { 10445 name string 10446 paid uint64 10447 init, swapWallets bool 10448 tfpErr, updateOrderErr error 10449 }{{ 10450 name: "ok init", 10451 paid: 1, 10452 init: true, 10453 }, { 10454 name: "ok redeem", 10455 paid: 1, 10456 swapWallets: true, 10457 }, { 10458 name: "not dynamic", 10459 }, { 10460 name: "TransactionFeesPaid error other than coin not found", 10461 init: true, 10462 tfpErr: errors.New("other error"), 10463 }} 10464 for _, test := range tests { 10465 acctWallet, tWallet := newTWallet(tACCTAsset.ID) 10466 dynamicFeeChecker := &TDynamicSwapper{TXCWallet: tWallet} 10467 acctWallet.Wallet = dynamicFeeChecker 10468 tWallet.confs["00"] = 10 10469 10470 utxoWallet, _ := newTWallet(tUTXOAssetA.ID) 10471 10472 wallets := &walletSet{ 10473 fromWallet: acctWallet, 10474 toWallet: utxoWallet, 10475 baseWallet: acctWallet, 10476 quoteWallet: utxoWallet, 10477 } 10478 if test.swapWallets { 10479 wallets.fromWallet, wallets.toWallet = wallets.toWallet, wallets.fromWallet 10480 wallets.baseWallet, wallets.quoteWallet = wallets.quoteWallet, wallets.baseWallet 10481 } 10482 10483 dc := &dexConnection{ 10484 acct: tNewAccount(&tCrypter{}), 10485 log: tLogger, 10486 } 10487 lo, _, _, _ := makeLimitOrder(dc, true, 0, 0) 10488 tracker := &trackedTrade{ 10489 wallets: wallets, 10490 dc: dc, 10491 metaData: new(db.OrderMetaData), 10492 db: new(TDB), 10493 Order: lo, 10494 notify: func(Notification) {}, 10495 } 10496 tracker.SetTime(time.Now()) 10497 dynamicFeeChecker.tfpPaid = 1 10498 dynamicFeeChecker.tfpErr = test.tfpErr 10499 dynamicFeeChecker.tfpSecretHashes = [][]byte{{0}} 10500 matchID := ordertest.RandomMatchID() 10501 match := &matchTracker{ 10502 MetaMatch: db.MetaMatch{ 10503 UserMatch: &order.UserMatch{MatchID: matchID}, 10504 MetaData: &db.MatchMetaData{ 10505 Proof: db.MatchProof{ 10506 TakerSwap: []byte{0}, 10507 MakerSwap: []byte{0}, 10508 MakerRedeem: []byte{0}, 10509 TakerRedeem: []byte{0}, 10510 SecretHash: []byte{0}, 10511 }, 10512 }, 10513 }, 10514 } 10515 tracker.updateDynamicSwapOrRedemptionFeesPaid(ctx, match, test.init) 10516 got := tracker.metaData.SwapFeesPaid 10517 if !test.init { 10518 got = tracker.metaData.RedemptionFeesPaid 10519 } 10520 if got != test.paid { 10521 t.Fatalf("%s: want %d but got %d fees paid", test.name, test.paid, got) 10522 } 10523 } 10524 } 10525 10526 func TestUpdateBondOptions(t *testing.T) { 10527 const feeRate = 50 10528 10529 rig := newTestRig() 10530 defer rig.shutdown() 10531 acct := rig.dc.acct 10532 acct.isAuthed = true 10533 10534 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 10535 dcrWallet.Wallet = &TFeeRater{tDcrWallet, feeRate} 10536 rig.core.wallets[tUTXOAssetA.ID] = dcrWallet 10537 bondFeeBuffer := tDcrWallet.BondsFeeBuffer(feeRate) 10538 10539 bondAsset := dcrBondAsset 10540 var wrongBondAssetID uint32 = 0 10541 var targetTier uint64 = 1 10542 var targetTierZero uint64 = 0 10543 defaultMaxBondedAmt := maxBondedMult * bondAsset.Amt * targetTier 10544 tooLowMaxBonded := defaultMaxBondedAmt - 1 10545 // Double because we will reserve for the bond that's about to be posted 10546 // in rotateBonds too. 10547 singlyBondedReserves := bondAsset.Amt*targetTier*2 + bondFeeBuffer 10548 10549 type acctState struct { 10550 targetTier uint64 10551 maxBondedAmt uint64 10552 } 10553 10554 for _, tt := range []struct { 10555 name string 10556 bal uint64 10557 form BondOptionsForm 10558 before acctState 10559 after acctState 10560 expReserves uint64 10561 addOtherDC bool 10562 wantErr bool 10563 }{ 10564 { 10565 name: "set target tier to 1", 10566 bal: singlyBondedReserves, 10567 form: BondOptionsForm{ 10568 Host: acct.host, 10569 TargetTier: &targetTier, 10570 BondAssetID: &bondAsset.ID, 10571 }, 10572 after: acctState{ 10573 targetTier: 1, 10574 maxBondedAmt: defaultMaxBondedAmt, 10575 }, 10576 expReserves: singlyBondedReserves, 10577 }, 10578 { 10579 name: "low balance", 10580 bal: singlyBondedReserves - 1, 10581 form: BondOptionsForm{ 10582 Host: acct.host, 10583 TargetTier: &targetTier, 10584 BondAssetID: &bondAsset.ID, 10585 }, 10586 wantErr: true, 10587 }, 10588 { 10589 name: "max-bonded too low", 10590 bal: singlyBondedReserves, 10591 form: BondOptionsForm{ 10592 Host: acct.host, 10593 TargetTier: &targetTier, 10594 BondAssetID: &bondAsset.ID, 10595 MaxBondedAmt: &tooLowMaxBonded, 10596 }, 10597 wantErr: true, 10598 }, 10599 { 10600 name: "unsupported bond asset", 10601 form: BondOptionsForm{ 10602 Host: acct.host, 10603 TargetTier: &targetTier, 10604 BondAssetID: &wrongBondAssetID, 10605 }, 10606 wantErr: true, 10607 }, 10608 { 10609 name: "lower target tier with zero balance OK", 10610 bal: 0, 10611 form: BondOptionsForm{ 10612 Host: acct.host, 10613 TargetTier: &targetTierZero, 10614 BondAssetID: &bondAsset.ID, 10615 }, 10616 before: acctState{ 10617 targetTier: 1, 10618 maxBondedAmt: defaultMaxBondedAmt, 10619 }, 10620 after: acctState{}, 10621 expReserves: 0, 10622 }, 10623 { 10624 name: "lower target tier to zero with other exchanges still keeps reserves", 10625 bal: 0, 10626 form: BondOptionsForm{ 10627 Host: acct.host, 10628 TargetTier: &targetTierZero, 10629 BondAssetID: &bondAsset.ID, 10630 }, 10631 before: acctState{ 10632 targetTier: 1, 10633 maxBondedAmt: defaultMaxBondedAmt, 10634 }, 10635 addOtherDC: true, 10636 after: acctState{}, 10637 expReserves: bondFeeBuffer, 10638 }, 10639 } { 10640 t.Run(tt.name, func(t *testing.T) { 10641 before, after := tt.before, tt.after 10642 acct.targetTier = before.targetTier 10643 acct.maxBondedAmt = before.maxBondedAmt 10644 tDcrWallet.bal = &asset.Balance{Available: tt.bal} 10645 10646 if tt.addOtherDC { 10647 dc, _, acct := testDexConnection(rig.core.ctx, rig.crypter.(*tCrypter)) 10648 acct.host = "someotherhost.com" 10649 rig.core.conns[acct.host] = dc 10650 defer delete(rig.core.conns, acct.host) 10651 acct.bondAsset = bondAsset.ID 10652 acct.targetTier = 1 10653 } 10654 10655 if err := rig.core.UpdateBondOptions(&tt.form); err != nil { 10656 if tt.wantErr { 10657 return 10658 } 10659 t.Fatalf("UpdateBondOptions error: %v", err) 10660 } 10661 if tt.wantErr { 10662 t.Fatalf("No error when one was expected") 10663 } 10664 10665 if acct.targetTier != after.targetTier { 10666 t.Fatalf("Wrong targetTier. %d != %d", acct.targetTier, after.targetTier) 10667 } 10668 if acct.maxBondedAmt != after.maxBondedAmt { 10669 t.Fatalf("Wrong maxBondedAmt. %d != %d", acct.maxBondedAmt, after.maxBondedAmt) 10670 } 10671 if tDcrWallet.reserves.Load() != tt.expReserves { 10672 t.Fatalf("Wrong reserves. %d != %d", tDcrWallet.reserves.Load(), tt.expReserves) 10673 } 10674 }) 10675 } 10676 } 10677 10678 func TestRotateBonds(t *testing.T) { 10679 const feeRate = 50 10680 10681 rig := newTestRig() 10682 defer rig.shutdown() 10683 rig.core.Login(tPW) 10684 10685 acct := rig.dc.acct 10686 acct.isAuthed = true 10687 10688 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 10689 dcrWallet.Wallet = &TFeeRater{tDcrWallet, feeRate} 10690 rig.core.wallets[tUTXOAssetA.ID] = dcrWallet 10691 bondAsset := dcrBondAsset 10692 bondFeeBuffer := tDcrWallet.BondsFeeBuffer(feeRate) 10693 maxBondedPerTier := maxBondedMult * bondAsset.Amt 10694 10695 now := uint64(time.Now().Unix()) 10696 bondExpiry := rig.dc.config().BondExpiry 10697 // bondDuration := minBondLifetime(rig.core.net, bondExpiry) 10698 locktimeThresh := now + bondExpiry 10699 pBuffer := uint64(pendingBuffer(rig.core.net)) 10700 mergeableLocktimeThresh := locktimeThresh + bondExpiry/4 + pBuffer 10701 // unexpired := locktimeThresh + 1 10702 locktimeExpired := locktimeThresh - 1 10703 locktimeRefundable := now - 1 10704 weakTimeThresh := locktimeThresh + pBuffer 10705 10706 run := func(wantPending, wantExpired int, expectedReserves uint64) { 10707 ctx, cancel := context.WithTimeout(rig.core.ctx, time.Second) 10708 rig.core.rotateBonds(ctx) 10709 cancel() 10710 10711 t.Helper() 10712 if len(acct.pendingBonds) != wantPending { 10713 t.Fatalf("wanted %d pending bonds, got %d", wantPending, len(acct.pendingBonds)) 10714 } 10715 if len(acct.expiredBonds) != wantExpired { 10716 t.Fatalf("wanted %d expired bonds, got %d", wantExpired, len(acct.expiredBonds)) 10717 } 10718 if tDcrWallet.reserves.Load() != expectedReserves { 10719 t.Fatalf("wrong reserves. expected %d, got %d", expectedReserves, tDcrWallet.reserves.Load()) 10720 } 10721 } 10722 10723 // No bonds, target tier 1. Should create a new bond and add it to pending. 10724 var targetTier uint64 = 1 10725 acct.targetTier = targetTier 10726 acct.maxBondedAmt = maxBondedPerTier * targetTier 10727 acct.bondAsset = bondAsset.ID 10728 tDcrWallet.bal = &asset.Balance{Available: bondAsset.Amt*targetTier + bondFeeBuffer} 10729 rig.queuePrevalidateBond() 10730 run(1, 0, bondAsset.Amt+bondFeeBuffer) 10731 10732 // Post and then expire the bond. This first bond should move to expired and we 10733 // should create another bond. 10734 acct.bonds, acct.pendingBonds = acct.pendingBonds, nil 10735 acct.bonds[0].LockTime = locktimeExpired 10736 rig.queuePrevalidateBond() 10737 // The newly expired bond will be refunded in time to fund our next round, 10738 // so we only need fees reserved. 10739 run(1, 1, bondFeeBuffer) 10740 10741 // If the live bond is closer to expiration, the expired bond won't be 10742 // ready in time, so we'll need more reserves. 10743 acct.bonds, acct.pendingBonds = acct.pendingBonds, nil 10744 acct.bonds[0].LockTime = weakTimeThresh + 1 10745 run(0, 1, bondAsset.Amt+bondFeeBuffer) 10746 10747 // Make the live bond weak. Should get a pending bond. Only fees reserves, 10748 // because we still have an expired bond. 10749 acct.bonds[0].LockTime = weakTimeThresh - 1 10750 rig.queuePrevalidateBond() 10751 run(1, 1, bondFeeBuffer) 10752 10753 // Refund the expired bond 10754 acct.expiredBonds[0].LockTime = locktimeRefundable 10755 tDcrWallet.contractExpired = true 10756 tDcrWallet.refundBondCoin = &tCoin{} 10757 run(1, 0, bondAsset.Amt+bondFeeBuffer) 10758 10759 acct.targetTier = 2 10760 acct.bonds = nil 10761 rig.queuePrevalidateBond() 10762 run(2, 0, bondAsset.Amt*2+bondFeeBuffer) 10763 10764 // Check that a new bond will be scheduled for merge with an existing bond 10765 // if the locktime is not too soon. 10766 acct.bonds = append(acct.bonds, acct.pendingBonds[0]) 10767 acct.pendingBonds = nil 10768 acct.bonds[0].LockTime = mergeableLocktimeThresh + 5 10769 rig.queuePrevalidateBond() 10770 run(1, 0, 2*bondAsset.Amt+bondFeeBuffer) 10771 mergingBond := acct.pendingBonds[0] 10772 if mergingBond.LockTime != acct.bonds[0].LockTime { 10773 t.Fatalf("Mergeable bond was not merged") 10774 } 10775 10776 // Same thing, but without the merge, just to check our threshold calc. 10777 acct.pendingBonds = nil 10778 acct.bonds[0].LockTime = mergeableLocktimeThresh - 1 10779 rig.queuePrevalidateBond() 10780 run(1, 0, 2*bondAsset.Amt+bondFeeBuffer) 10781 unmergingBond := acct.pendingBonds[0] 10782 if unmergingBond.LockTime == acct.bonds[0].LockTime { 10783 t.Fatalf("Unmergeable bond was scheduled for merged") 10784 } 10785 } 10786 10787 func TestFindBondKeyIdx(t *testing.T) { 10788 rig := newTestRig() 10789 defer rig.shutdown() 10790 rig.core.Login(tPW) 10791 10792 pkhEqualFnFn := func(find bool) func(bondKey *secp256k1.PrivateKey) bool { 10793 return func(bondKey *secp256k1.PrivateKey) bool { 10794 return find 10795 } 10796 } 10797 tests := []struct { 10798 name string 10799 pkhEqualFn func(bondKey *secp256k1.PrivateKey) bool 10800 wantErr bool 10801 }{{ 10802 name: "ok", 10803 pkhEqualFn: pkhEqualFnFn(true), 10804 }, { 10805 name: "cant find", 10806 pkhEqualFn: pkhEqualFnFn(false), 10807 wantErr: true, 10808 }} 10809 10810 for _, test := range tests { 10811 t.Run(test.name, func(t *testing.T) { 10812 _, err := rig.core.findBondKeyIdx(test.pkhEqualFn, 0) 10813 if test.wantErr { 10814 if err == nil { 10815 t.Fatal("expected error") 10816 } 10817 return 10818 } 10819 if err != nil { 10820 t.Fatalf("unexpected error: %v", err) 10821 } 10822 }) 10823 } 10824 } 10825 10826 func TestFindBond(t *testing.T) { 10827 rig := newTestRig() 10828 defer rig.shutdown() 10829 dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID) 10830 rig.core.wallets[tUTXOAssetA.ID] = dcrWallet 10831 rig.core.Login(tPW) 10832 10833 bd := &asset.BondDetails{ 10834 Bond: &asset.Bond{ 10835 Amount: tFee, 10836 AssetID: tUTXOAssetA.ID, 10837 }, 10838 LockTime: time.Now(), 10839 CheckPrivKey: func(bondKey *secp256k1.PrivateKey) bool { 10840 return true 10841 }, 10842 } 10843 msgBond := &msgjson.Bond{ 10844 Version: 0, 10845 AssetID: tUTXOAssetA.ID, 10846 } 10847 10848 tests := []struct { 10849 name string 10850 findBond *asset.BondDetails 10851 findBondErr error 10852 wantStr uint32 10853 }{{ 10854 name: "ok", 10855 findBond: bd, 10856 wantStr: 1, 10857 }, { 10858 name: "find bond error", 10859 findBondErr: errors.New("some error"), 10860 }} 10861 10862 for _, test := range tests { 10863 t.Run(test.name, func(t *testing.T) { 10864 tDcrWallet.findBond = test.findBond 10865 tDcrWallet.findBondErr = test.findBondErr 10866 str, _ := rig.core.findBond(rig.dc, msgBond) 10867 if str != test.wantStr { 10868 t.Fatalf("wanted str %d but got %d", test.wantStr, str) 10869 } 10870 }) 10871 } 10872 } 10873 10874 func TestNetworkFeeRate(t *testing.T) { 10875 rig := newTestRig() 10876 defer rig.shutdown() 10877 10878 assetID := tUTXOAssetA.ID 10879 wallet, tWallet := newTWallet(assetID) 10880 rig.core.wallets[assetID] = wallet 10881 10882 const feeRaterRate = 50 10883 dumbWallet := wallet.Wallet 10884 wallet.Wallet = &TFeeRater{ 10885 TXCWallet: tWallet, 10886 feeRate: feeRaterRate, 10887 } 10888 if r := rig.core.NetworkFeeRate(assetID); r != feeRaterRate { 10889 t.Fatalf("FeeRater not working. %d != %d", r, feeRaterRate) 10890 } 10891 wallet.Wallet = dumbWallet 10892 10893 const bookFeedFeeRate = 60 10894 book := newBookie(rig.dc, assetID, tUTXOAssetB.ID, nil, tLogger) 10895 rig.dc.books[tDcrBtcMktName] = book 10896 book.logEpochReport(&msgjson.EpochReportNote{BaseFeeRate: bookFeedFeeRate}) 10897 if r := rig.core.NetworkFeeRate(assetID); r != bookFeedFeeRate { 10898 t.Fatalf("Book feed fee rate not working. %d != %d", r, bookFeedFeeRate) 10899 } 10900 delete(rig.dc.books, tDcrBtcMktName) 10901 10902 const serverFeeRate = 70 10903 rig.ws.queueResponse(msgjson.FeeRateRoute, func(msg *msgjson.Message, f msgFunc) error { 10904 resp, _ := msgjson.NewResponse(msg.ID, serverFeeRate, nil) 10905 f(resp) 10906 return nil 10907 }) 10908 if r := rig.core.NetworkFeeRate(assetID); r != serverFeeRate { 10909 t.Fatalf("Server fee rate not working. %d != %d", r, serverFeeRate) 10910 } 10911 } 10912 10913 func TestPokesCacheInit(t *testing.T) { 10914 tPokes := []*db.Notification{ 10915 {DetailText: "poke 1"}, 10916 {DetailText: "poke 2"}, 10917 {DetailText: "poke 3"}, 10918 {DetailText: "poke 4"}, 10919 {DetailText: "poke 5"}, 10920 } 10921 { 10922 pokesCapacity := 6 10923 c := newPokesCache(pokesCapacity) 10924 c.init(tPokes) 10925 10926 // Check if the cache is initialized correctly 10927 if len(c.cache) != 5 { 10928 t.Errorf("Expected cache length %d, got %d", len(tPokes), len(c.cache)) 10929 } 10930 10931 if c.cursor != 5 { 10932 t.Errorf("Expected cursor %d, got %d", len(tPokes)%pokesCapacity, c.cursor) 10933 } 10934 10935 // Check if the cache contains the correct pokes 10936 for i, poke := range tPokes { 10937 if c.cache[i] != poke { 10938 t.Errorf("Expected poke %v at index %d, got %v", poke, i, c.cache[i]) 10939 } 10940 } 10941 } 10942 { 10943 pokesCapacity := 4 10944 c := newPokesCache(pokesCapacity) 10945 c.init(tPokes) 10946 10947 // Check if the cache is initialized correctly 10948 if len(c.cache) != 1 { 10949 t.Errorf("Expected cache length %d, got %d", 1, len(c.cache)) 10950 } 10951 10952 if c.cursor != 1 { 10953 t.Errorf("Expected cursor %d, got %d", 1, c.cursor) 10954 } 10955 10956 // Check if the cache contains the correct pokes 10957 for i, poke := range tPokes[:len(tPokes)-pokesCapacity] { 10958 if c.cache[i] != poke { 10959 t.Errorf("Expected poke %v at index %d, got %v", poke, i, c.cache[i]) 10960 } 10961 } 10962 } 10963 } 10964 10965 func TestPokesAdd(t *testing.T) { 10966 tPokes := []*db.Notification{ 10967 {DetailText: "poke 1"}, 10968 {DetailText: "poke 2"}, 10969 {DetailText: "poke 3"}, 10970 {DetailText: "poke 4"}, 10971 {DetailText: "poke 5"}, 10972 } 10973 tNewPoke := &db.Notification{ 10974 DetailText: "poke 6", 10975 } 10976 { 10977 pokesCapacity := 6 10978 c := newPokesCache(pokesCapacity) 10979 c.init(tPokes) 10980 c.add(tNewPoke) 10981 10982 // Check if the cache is updated correctly 10983 if len(c.cache) != 6 { 10984 t.Errorf("Expected cache length %d, got %d", len(tPokes), len(c.cache)) 10985 } 10986 10987 if c.cursor != 0 { 10988 t.Errorf("Expected cursor %d, got %d", 0, c.cursor) 10989 } 10990 10991 // Check if the cache contains the correct pokes 10992 tAllPokes := append(tPokes, tNewPoke) 10993 for i, poke := range tAllPokes { 10994 if c.cache[i] != poke { 10995 t.Errorf("Expected poke %v at index %d, got %v", poke, i, c.cache[i]) 10996 } 10997 } 10998 } 10999 { 11000 pokesCapacity := 5 11001 c := newPokesCache(pokesCapacity) 11002 c.init(tPokes) 11003 c.add(tNewPoke) 11004 11005 // Check if the cache is updated correctly 11006 if len(c.cache) != pokesCapacity { 11007 t.Errorf("Expected cache length %d, got %d", pokesCapacity, len(c.cache)) 11008 } 11009 11010 if c.cursor != 1 { 11011 t.Errorf("Expected cursor %d, got %d", 1, c.cursor) 11012 } 11013 11014 // Check if the cache contains the correct pokes 11015 tAllPokes := make([]*db.Notification, 0) 11016 tAllPokes = append(tAllPokes, tNewPoke) 11017 tAllPokes = append(tAllPokes, tPokes[1:]...) 11018 for i, poke := range tAllPokes { 11019 if c.cache[i] != poke { 11020 t.Errorf("Expected poke %v at index %d, got %v", poke, i, c.cache[i]) 11021 } 11022 } 11023 } 11024 } 11025 11026 func TestPokesCachePokes(t *testing.T) { 11027 tPokes := []*db.Notification{ 11028 {TimeStamp: 1, DetailText: "poke 1"}, 11029 {TimeStamp: 2, DetailText: "poke 2"}, 11030 {TimeStamp: 3, DetailText: "poke 3"}, 11031 {TimeStamp: 4, DetailText: "poke 4"}, 11032 {TimeStamp: 5, DetailText: "poke 5"}, 11033 } 11034 { 11035 pokesCapacity := 6 11036 c := newPokesCache(pokesCapacity) 11037 c.init(tPokes) 11038 pokes := c.pokes() 11039 11040 // Check if the result length is correct 11041 if len(pokes) != len(tPokes) { 11042 t.Errorf("Expected pokes length %d, got %d", len(tPokes), len(pokes)) 11043 } 11044 11045 // Check if the result contains the correct pokes 11046 for i, poke := range tPokes { 11047 if pokes[i] != poke { 11048 t.Errorf("Expected poke %v at index %d, got %v", poke, i, pokes[i]) 11049 } 11050 } 11051 } 11052 { 11053 pokesCapacity := 5 11054 tNewPoke := &db.Notification{ 11055 TimeStamp: 6, 11056 DetailText: "poke 6", 11057 } 11058 c := newPokesCache(pokesCapacity) 11059 c.init(tPokes) 11060 c.add(tNewPoke) 11061 pokes := c.pokes() 11062 11063 // Check if the result length is correct 11064 if len(pokes) != pokesCapacity { 11065 t.Errorf("Expected cache length %d, got %d", 1, len(pokes)) 11066 } 11067 11068 tAllPokes := append(tPokes[1:], tNewPoke) 11069 // Check if the result contains the correct pokes 11070 for i, poke := range tAllPokes { 11071 if pokes[i] != poke { 11072 t.Errorf("Expected poke %v at index %d, got %v", poke, i, pokes[i]) 11073 } 11074 } 11075 } 11076 } 11077 11078 func TestTradingLimits(t *testing.T) { 11079 rig := newTestRig() 11080 defer rig.shutdown() 11081 11082 checkTradingLimits := func(expectedUserParcels, expectedParcelLimit uint32) { 11083 t.Helper() 11084 11085 userParcels, parcelLimit, err := rig.core.TradingLimits(tDexHost) 11086 if err != nil { 11087 t.Fatalf("unexpected error: %v", err) 11088 } 11089 11090 if userParcels != expectedUserParcels { 11091 t.Fatalf("expected user parcels %d, got %d", expectedUserParcels, userParcels) 11092 } 11093 11094 if parcelLimit != expectedParcelLimit { 11095 t.Fatalf("expected parcel limit %d, got %d", expectedParcelLimit, parcelLimit) 11096 } 11097 } 11098 11099 rig.dc.acct.rep.BondedTier = 10 11100 book := newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger) 11101 rig.dc.books[tDcrBtcMktName] = book 11102 checkTradingLimits(0, 20) 11103 11104 oids := []order.OrderID{ 11105 {0x01}, {0x02}, {0x03}, {0x04}, {0x05}, 11106 } 11107 11108 // Add an epoch order, 2 lots not likely taker 11109 ord := &order.LimitOrder{ 11110 Force: order.StandingTiF, 11111 P: order.Prefix{ServerTime: time.Now()}, 11112 T: order.Trade{ 11113 Sell: true, 11114 Quantity: dcrBtcLotSize * 2, 11115 }, 11116 } 11117 tracker := &trackedTrade{ 11118 Order: ord, 11119 preImg: newPreimage(), 11120 mktID: tDcrBtcMktName, 11121 db: rig.db, 11122 dc: rig.dc, 11123 metaData: &db.OrderMetaData{ 11124 Status: order.OrderStatusEpoch, 11125 }, 11126 } 11127 rig.dc.trades[oids[0]] = tracker 11128 checkTradingLimits(2, 20) 11129 11130 // Add another epoch order, 2 lots, likely taker, so 2x 11131 ord = &order.LimitOrder{ 11132 Force: order.ImmediateTiF, 11133 P: order.Prefix{ServerTime: time.Now()}, 11134 T: order.Trade{ 11135 Sell: true, 11136 Quantity: dcrBtcLotSize * 2, 11137 }, 11138 } 11139 tracker = &trackedTrade{ 11140 Order: ord, 11141 preImg: newPreimage(), 11142 mktID: tDcrBtcMktName, 11143 db: rig.db, 11144 dc: rig.dc, 11145 metaData: &db.OrderMetaData{ 11146 Status: order.OrderStatusEpoch, 11147 }, 11148 } 11149 rig.dc.trades[oids[1]] = tracker 11150 checkTradingLimits(6, 20) 11151 11152 // Add partially filled booked order 11153 ord = &order.LimitOrder{ 11154 P: order.Prefix{ServerTime: time.Now()}, 11155 T: order.Trade{ 11156 Sell: true, 11157 Quantity: dcrBtcLotSize * 2, 11158 FillAmt: dcrBtcLotSize, 11159 }, 11160 } 11161 tracker = &trackedTrade{ 11162 Order: ord, 11163 preImg: newPreimage(), 11164 mktID: tDcrBtcMktName, 11165 db: rig.db, 11166 dc: rig.dc, 11167 metaData: &db.OrderMetaData{ 11168 Status: order.OrderStatusBooked, 11169 }, 11170 } 11171 rig.dc.trades[oids[2]] = tracker 11172 checkTradingLimits(7, 20) 11173 11174 // Add settling match to the booked order 11175 tracker.matches = map[order.MatchID]*matchTracker{ 11176 {0x01}: { 11177 MetaMatch: db.MetaMatch{ 11178 UserMatch: &order.UserMatch{ 11179 Quantity: dcrBtcLotSize, 11180 }, 11181 MetaData: &db.MatchMetaData{ 11182 Proof: db.MatchProof{}, 11183 }, 11184 }, 11185 }, 11186 } 11187 checkTradingLimits(8, 20) 11188 } 11189 11190 func TestTakeAction(t *testing.T) { 11191 rig := newTestRig() 11192 defer rig.shutdown() 11193 11194 coinID := encode.RandomBytes(32) 11195 uniqueID := dex.Bytes(coinID).String() 11196 11197 newMatch := func() *matchTracker { 11198 var matchID order.MatchID 11199 copy(matchID[:], encode.RandomBytes(32)) 11200 return &matchTracker{ 11201 MetaMatch: db.MetaMatch{ 11202 UserMatch: &order.UserMatch{ 11203 Status: order.MatchComplete, 11204 MatchID: matchID, 11205 Side: order.Taker, 11206 }, 11207 MetaData: &db.MatchMetaData{}, 11208 }, 11209 } 11210 } 11211 rightMatch := newMatch() 11212 rightMatch.MetaData.Proof.TakerRedeem = coinID 11213 rightMatch.redemptionRejected = true 11214 11215 wrongMatch := newMatch() 11216 wrongMatch.MetaData.Proof.TakerRedeem = encode.RandomBytes(31) 11217 11218 makerMatch := newMatch() 11219 makerMatch.Status = order.MakerRedeemed 11220 makerMatch.MetaData.Proof.MakerRedeem = coinID 11221 makerMatch.Side = order.Maker 11222 11223 tracker := &trackedTrade{ 11224 matches: map[order.MatchID]*matchTracker{ 11225 rightMatch.MatchID: rightMatch, 11226 wrongMatch.MatchID: wrongMatch, 11227 makerMatch.MatchID: makerMatch, 11228 }, 11229 } 11230 11231 var oid order.OrderID 11232 copy(oid[:], encode.RandomBytes(32)) 11233 11234 rig.dc.trades[oid] = tracker 11235 11236 requestData := []byte(fmt.Sprintf(`{"orderID":"abcd","coinID":"%s","retry":true}`, dex.Bytes(coinID))) 11237 11238 err := rig.core.TakeAction(0, ActionIDRedeemRejected, requestData) 11239 if err == nil { 11240 t.Fatalf("expected error for wrong order ID but got nothing") 11241 } 11242 11243 rig.core.requestedActions[uniqueID] = nil 11244 requestData = []byte(fmt.Sprintf(`{"orderID":"%s","coinID":"%s","retry":false}`, oid, dex.Bytes(coinID))) 11245 11246 err = rig.core.TakeAction(0, ActionIDRedeemRejected, requestData) 11247 if err != nil { 11248 t.Fatalf("error for retry=false: %v", err) 11249 } 11250 if len(rig.core.requestedActions) != 0 { 11251 t.Fatal("requested action not removed") 11252 } 11253 11254 requestData = []byte(fmt.Sprintf(`{"orderID":"%s","coinID":"%s","retry":true}`, oid, dex.Bytes(coinID))) 11255 err = rig.core.TakeAction(0, ActionIDRedeemRejected, requestData) 11256 if err != nil { 11257 t.Fatalf("error for taker retry=true: %v", err) 11258 } 11259 11260 if len(rightMatch.MetaData.Proof.TakerRedeem) != 0 { 11261 t.Fatalf("taker redemption not cleared") 11262 } 11263 if len(wrongMatch.MetaData.Proof.TakerRedeem) == 0 { 11264 t.Fatalf("wrong taker redemption cleared") 11265 } 11266 11267 makerMatch.redemptionRejected = true 11268 err = rig.core.TakeAction(0, ActionIDRedeemRejected, requestData) 11269 if err != nil { 11270 t.Fatalf("error for maker retry=true: %v", err) 11271 } 11272 if len(makerMatch.MetaData.Proof.MakerRedeem) != 0 { 11273 t.Fatalf("maker redemption not cleared") 11274 } 11275 11276 }