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