From 92ec499e51121340a15116e13c9fc3de46764de4 Mon Sep 17 00:00:00 2001 From: JoeGruffins <34998433+JoeGruffins@users.noreply.github.com> Date: Fri, 11 Oct 2024 21:19:41 +0900 Subject: [PATCH 1/7] dcr: Remove tx history wait. (#3005) SyncStatus has a chance to hold the tipMtx so run it in a goroutine and do not wait. --- client/asset/dcr/dcr.go | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/client/asset/dcr/dcr.go b/client/asset/dcr/dcr.go index 87481252a9..7049dd6965 100644 --- a/client/asset/dcr/dcr.go +++ b/client/asset/dcr/dcr.go @@ -5936,7 +5936,7 @@ func isMixTx(tx *wire.MsgTx) (isMix bool, mixDenom int64) { } // idUnknownTx identifies the type and details of a transaction either made -// or recieved by the wallet. +// or received by the wallet. func (dcr *ExchangeWallet) idUnknownTx(ctx context.Context, tx *ListTransactionsResult) (*asset.WalletTransaction, error) { txHash, err := chainhash.NewHashFromStr(tx.TxID) if err != nil { @@ -6252,7 +6252,7 @@ func (dcr *ExchangeWallet) idUnknownTx(ctx context.Context, tx *ListTransactions } // addUnknownTransactionsToHistory checks for any transactions the wallet has -// made or recieved that are not part of the transaction history. It scans +// made or received that are not part of the transaction history. It scans // from the last point to which it had previously scanned to the current tip. func (dcr *ExchangeWallet) addUnknownTransactionsToHistory(tip uint64) { txHistoryDB := dcr.txDB() @@ -6320,7 +6320,7 @@ func (dcr *ExchangeWallet) addUnknownTransactionsToHistory(tip uint64) { } // syncTxHistory checks to see if there are any transactions which the wallet -// has made or recieved that are not part of the transaction history, then +// has made or received that are not part of the transaction history, then // identifies and adds them. It also checks all the pending transactions to see // if they have been mined into a block, and if so, updates the transaction // history to reflect the block height. @@ -6699,11 +6699,10 @@ func (dcr *ExchangeWallet) handleTipChange(ctx context.Context, newTipHash *chai dcr.cycleMixer() } - var wg sync.WaitGroup - wg.Add(1) + dcr.wg.Add(1) go func() { - defer wg.Done() dcr.syncTxHistory(ctx, uint64(newTipHeight)) + dcr.wg.Done() }() // Search for contract redemption in new blocks if there @@ -6759,13 +6758,7 @@ func (dcr *ExchangeWallet) handleTipChange(ctx context.Context, newTipHash *chai // Run the redemption search from the startHeight determined above up // till the current tip height. - wg.Add(1) - go func() { - defer wg.Done() - dcr.findRedemptionsInBlockRange(startHeight, newTipHeight, contractOutpoints) - }() - - wg.Wait() + dcr.findRedemptionsInBlockRange(startHeight, newTipHeight, contractOutpoints) } func (dcr *ExchangeWallet) getBestBlock(ctx context.Context) (*block, error) { From e291957f8a97c500bb711a22196d2e99a658ee5a Mon Sep 17 00:00:00 2001 From: buck54321 Date: Fri, 11 Oct 2024 07:20:03 -0500 Subject: [PATCH 2/7] core: protect epoch checksum with its own mutex (#2977) * match checksum to separate mutex and add dc cancel tracking --- client/core/core.go | 113 ++++++++++++++++++++++++------------ client/core/core_test.go | 101 +++++++++++++++++++------------- client/core/simnet_trade.go | 2 +- client/core/trade.go | 53 ++++++++++++----- 4 files changed, 176 insertions(+), 93 deletions(-) diff --git a/client/core/core.go b/client/core/core.go index 8527266925..e80ec38351 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -173,6 +173,10 @@ type dexConnection struct { // processed by a dex server. inFlightOrders map[uint64]*InFlightOrder + // A map linking cancel order IDs to trade order IDs. + cancelsMtx sync.RWMutex + cancels map[order.OrderID]order.OrderID + blindCancelsMtx sync.Mutex blindCancels map[order.OrderID]order.Preimage @@ -253,6 +257,25 @@ func (dc *dexConnection) bondAssets() (map[uint32]*BondAsset, uint64) { return bondAssets, cfg.BondExpiry } +func (dc *dexConnection) registerCancelLink(cid, oid order.OrderID) { + dc.cancelsMtx.Lock() + dc.cancels[cid] = oid + dc.cancelsMtx.Unlock() +} + +func (dc *dexConnection) deleteCancelLink(cid order.OrderID) { + dc.cancelsMtx.Lock() + delete(dc.cancels, cid) + dc.cancelsMtx.Unlock() +} + +func (dc *dexConnection) cancelTradeID(cid order.OrderID) (order.OrderID, bool) { + dc.cancelsMtx.RLock() + defer dc.cancelsMtx.RUnlock() + oid, found := dc.cancels[cid] + return oid, found +} + // marketConfig is the market's configuration, as returned by the server in the // 'config' response. func (dc *dexConnection) marketConfig(mktID string) *msgjson.Market { @@ -577,17 +600,19 @@ func (dc *dexConnection) activeOrders() ([]*Order, []*InFlightOrder) { // findOrder returns the tracker and preimage for an order ID, and a boolean // indicating whether this is a cancel order. -func (dc *dexConnection) findOrder(oid order.OrderID) (tracker *trackedTrade, preImg order.Preimage, isCancel bool) { +func (dc *dexConnection) findOrder(oid order.OrderID) (tracker *trackedTrade, isCancel bool) { dc.tradeMtx.RLock() defer dc.tradeMtx.RUnlock() // Try to find the order as a trade. if tracker, found := dc.trades[oid]; found { - return tracker, tracker.preImg, false + return tracker, false } - // Search the cancel order IDs. - for _, tracker := range dc.trades { - if tracker.cancel != nil && tracker.cancel.ID() == oid { - return tracker, tracker.cancel.preImg, true + + if tid, found := dc.cancelTradeID(oid); found { + if tracker, found := dc.trades[tid]; found { + return tracker, true + } else { + dc.log.Errorf("Did not find trade for cancel order ID %s", oid) } } return @@ -645,7 +670,7 @@ func (c *Core) sendCancelOrder(dc *dexConnection, oid order.OrderID, base, quote // tryCancel will look for an order with the specified order ID, and attempt to // cancel the order. It is not an error if the order is not found. func (c *Core) tryCancel(dc *dexConnection, oid order.OrderID) (found bool, err error) { - tracker, _, _ := dc.findOrder(oid) + tracker, _ := dc.findOrder(oid) if tracker == nil { return // false, nil } @@ -771,7 +796,7 @@ func (dc *dexConnection) parseMatches(msgMatches []*msgjson.Match, checkSigs boo for _, msgMatch := range msgMatches { var oid order.OrderID copy(oid[:], msgMatch.OrderID) - tracker, _, isCancel := dc.findOrder(oid) + tracker, isCancel := dc.findOrder(oid) if tracker == nil { dc.blindCancelsMtx.Lock() _, found := dc.blindCancels[oid] @@ -4953,7 +4978,7 @@ func (c *Core) Order(oidB dex.Bytes) (*Order, error) { } // See if it's an active order first. for _, dc := range c.dexConnections() { - tracker, _, _ := dc.findOrder(oid) + tracker, _ := dc.findOrder(oid) if tracker != nil { return tracker.coreOrder(), nil } @@ -8104,6 +8129,7 @@ func (c *Core) newDEXConnection(acctInfo *db.AccountInfo, flag connectDEXFlag) ( ticker: newDexTicker(defaultTickInterval), // updated when server config obtained books: make(map[string]*bookie), trades: make(map[order.OrderID]*trackedTrade), + cancels: make(map[order.OrderID]order.OrderID), inFlightOrders: make(map[uint64]*InFlightOrder), blindCancels: make(map[order.OrderID]order.Preimage), apiVer: -1, @@ -8509,7 +8535,7 @@ func handleRevokeOrderMsg(c *Core, dc *dexConnection, msg *msgjson.Message) erro var oid order.OrderID copy(oid[:], revocation.OrderID) - tracker, _, isCancel := dc.findOrder(oid) + tracker, isCancel := dc.findOrder(oid) if tracker == nil { return fmt.Errorf("no order found with id %s", oid.String()) } @@ -8551,7 +8577,7 @@ func handleRevokeMatchMsg(c *Core, dc *dexConnection, msg *msgjson.Message) erro var oid order.OrderID copy(oid[:], revocation.OrderID) - tracker, _, _ := dc.findOrder(oid) + tracker, _ := dc.findOrder(oid) if tracker == nil { return fmt.Errorf("no order found with id %s (not an error if you've completed your side of the swap)", oid.String()) } @@ -8916,7 +8942,7 @@ func handlePreimageRequest(c *Core, dc *dexConnection, msg *msgjson.Message) err } if len(req.Commitment) != order.CommitmentSize { - return fmt.Errorf("received preimage request for %v with no corresponding order submission response.", oid) + return fmt.Errorf("received preimage request for %s with no corresponding order submission response", oid) } // See if we recognize that commitment, and if we do, just wait for the @@ -8950,7 +8976,8 @@ func handlePreimageRequest(c *Core, dc *dexConnection, msg *msgjson.Message) err } func processPreimageRequest(c *Core, dc *dexConnection, reqID uint64, oid order.OrderID, commitChecksum dex.Bytes) error { - tracker, preImg, isCancel := dc.findOrder(oid) + tracker, isCancel := dc.findOrder(oid) + var preImg order.Preimage if tracker == nil { var found bool dc.blindCancelsMtx.Lock() @@ -8962,7 +8989,8 @@ func processPreimageRequest(c *Core, dc *dexConnection, reqID uint64, oid order. } else { // Record the csum if this preimage request is novel, and deny it if // this is a duplicate request with an altered csum. - if !acceptCsum(tracker, isCancel, commitChecksum) { + var accept bool + if accept, preImg = acceptCsum(tracker, isCancel, commitChecksum); !accept { csumErr := errors.New("invalid csum in duplicate preimage request") resp, err := msgjson.NewResponse(reqID, nil, msgjson.NewError(msgjson.InvalidRequestError, "%v", csumErr)) @@ -9005,26 +9033,25 @@ func processPreimageRequest(c *Core, dc *dexConnection, reqID uint64, oid order. // the server may have used the knowledge of this preimage we are sending them // now to alter the epoch shuffle. The return value is false if a previous // checksum has been recorded that differs from the provided one. -func acceptCsum(tracker *trackedTrade, isCancel bool, commitChecksum dex.Bytes) bool { +func acceptCsum(tracker *trackedTrade, isCancel bool, commitChecksum dex.Bytes) (bool, order.Preimage) { // Do not allow csum to be changed once it has been committed to // (initialized to something other than `nil`) because it is probably a // malicious behavior by the server. - tracker.mtx.Lock() - defer tracker.mtx.Unlock() - + tracker.csumMtx.Lock() + defer tracker.csumMtx.Unlock() if isCancel { - if tracker.cancel.csum == nil { - tracker.cancel.csum = commitChecksum - return true + if tracker.cancelCsum == nil { + tracker.cancelCsum = commitChecksum + return true, tracker.cancelPreimg } - return bytes.Equal(commitChecksum, tracker.cancel.csum) + return bytes.Equal(commitChecksum, tracker.cancelCsum), tracker.cancelPreimg } if tracker.csum == nil { tracker.csum = commitChecksum - return true + return true, tracker.preImg } - return bytes.Equal(commitChecksum, tracker.csum) + return bytes.Equal(commitChecksum, tracker.csum), tracker.preImg } // handleMatchRoute processes the DEX-originating match route request, @@ -9103,7 +9130,7 @@ func handleNoMatchRoute(c *Core, dc *dexConnection, msg *msgjson.Message) error var oid order.OrderID copy(oid[:], nomatchMsg.OrderID) - tracker, _, _ := dc.findOrder(oid) + tracker, _ := dc.findOrder(oid) if tracker == nil { dc.blindCancelsMtx.Lock() _, found := dc.blindCancels[oid] @@ -9177,7 +9204,7 @@ func handleAuditRoute(c *Core, dc *dexConnection, msg *msgjson.Message) error { var oid order.OrderID copy(oid[:], audit.OrderID) - tracker, _, _ := dc.findOrder(oid) + tracker, _ := dc.findOrder(oid) if tracker == nil { return fmt.Errorf("audit request received for unknown order: %s", string(msg.Payload)) } @@ -9202,7 +9229,7 @@ func handleRedemptionRoute(c *Core, dc *dexConnection, msg *msgjson.Message) err var oid order.OrderID copy(oid[:], redemption.OrderID) - tracker, _, isCancel := dc.findOrder(oid) + tracker, isCancel := dc.findOrder(oid) if tracker != nil { if isCancel { return fmt.Errorf("redemption request received for cancel order %v, match %v (you ok server?)", @@ -10253,7 +10280,7 @@ func (c *Core) RemoveWalletPeer(assetID uint32, address string) error { // id. An error is returned if it cannot be found. func (c *Core) findActiveOrder(oid order.OrderID) (*trackedTrade, error) { for _, dc := range c.dexConnections() { - tracker, _, _ := dc.findOrder(oid) + tracker, _ := dc.findOrder(oid) if tracker != nil { return tracker, nil } @@ -10640,7 +10667,7 @@ func (c *Core) handleRetryRedemptionAction(actionB []byte) error { copy(oid[:], req.OrderID) var tracker *trackedTrade for _, dc := range c.dexConnections() { - tracker, _, _ = dc.findOrder(oid) + tracker, _ = dc.findOrder(oid) if tracker != nil { break } @@ -10738,6 +10765,15 @@ func (c *Core) checkEpochResolution(host string, mktID string) { } currentEpoch := dc.marketEpoch(mktID, time.Now()) lastEpoch := currentEpoch - 1 + + // Short path if we're already resolved. + dc.epochMtx.RLock() + resolvedEpoch := dc.resolvedEpoch[mktID] + dc.epochMtx.RUnlock() + if lastEpoch == resolvedEpoch { + return + } + ts, inFlights := dc.marketTrades(mktID) for _, ord := range inFlights { if ord.Epoch == lastEpoch { @@ -10745,18 +10781,23 @@ func (c *Core) checkEpochResolution(host string, mktID string) { } } for _, t := range ts { + // Is this order from the last epoch and still not booked or executed? if t.epochIdx() == lastEpoch && t.status() == order.OrderStatusEpoch { return } - if t.cancel != nil && t.cancelEpochIdx() == lastEpoch { - t.mtx.RLock() - matched := t.cancel.matches.taker != nil - t.mtx.RUnlock() - if !matched { - return - } + // Does this order have an in-flight cancel order that is not yet + // resolved? + t.mtx.RLock() + unresolvedCancel := t.cancel != nil && t.cancelEpochIdx() == lastEpoch && t.cancel.matches.taker == nil + t.mtx.RUnlock() + if unresolvedCancel { + return } } + + // We don't have any unresolved orders or cancel orders from the last epoch. + // Just make sure that not other thread has resolved the epoch and then send + // the notification. dc.epochMtx.Lock() sendUpdate := lastEpoch > dc.resolvedEpoch[mktID] dc.resolvedEpoch[mktID] = lastEpoch diff --git a/client/core/core_test.go b/client/core/core_test.go index 8f6fa9c5b0..95a73ddfb1 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -271,6 +271,7 @@ func testDexConnection(ctx context.Context, crypter *tCrypter) (*dexConnection, }, notify: func(Notification) {}, trades: make(map[order.OrderID]*trackedTrade), + cancels: make(map[order.OrderID]order.OrderID), inFlightOrders: make(map[uint64]*InFlightOrder), epoch: map[string]uint64{tDcrBtcMktName: 0}, resolvedEpoch: map[string]uint64{tDcrBtcMktName: 0}, @@ -3792,9 +3793,9 @@ func TestHandlePreimageRequest(t *testing.T) { // resetCsum resets csum for further preimage request since multiple // testing scenarios use the same tracker object. resetCsum := func(tracker *trackedTrade) { - tracker.mtx.Lock() + tracker.csumMtx.Lock() tracker.csum = nil - tracker.mtx.Unlock() + tracker.csumMtx.Unlock() } rig.dc.trades[oid] = tracker @@ -3923,15 +3924,17 @@ func TestHandlePreimageRequest(t *testing.T) { t.Fatal("no order note from preimage request handling") } - tracker.mtx.RLock() - if !bytes.Equal(commitCSum, tracker.csum) { + tracker.csumMtx.RLock() + csum := tracker.csum + tracker.csumMtx.RUnlock() + if !bytes.Equal(commitCSum, csum) { t.Fatalf( "handlePreimageRequest must initialize tracker csum, exp: %s, got: %s", commitCSum, - tracker.csum, + csum, ) } - tracker.mtx.RUnlock() + }) t.Run("more than one preimage request for order (different csums)", func(t *testing.T) { rig := newTestRig() @@ -3996,15 +3999,17 @@ func TestHandlePreimageRequest(t *testing.T) { t.Fatal("no msgjson.Error sent from preimage request handling") } - tracker.mtx.RLock() - if !bytes.Equal(firstCSum, tracker.csum) { + tracker.csumMtx.RLock() + csum := tracker.csum + tracker.csumMtx.RUnlock() + if !bytes.Equal(firstCSum, csum) { t.Fatalf( "[handlePreimageRequest] csum was changed, exp: %s, got: %s", firstCSum, - tracker.csum, + csum, ) } - tracker.mtx.RUnlock() + }) t.Run("more than one preimage request for order (same csum)", func(t *testing.T) { rig := newTestRig() @@ -4065,15 +4070,16 @@ func TestHandlePreimageRequest(t *testing.T) { t.Fatal("no order note from preimage request handling") } - tracker.mtx.RLock() - if !bytes.Equal(csum, tracker.csum) { + tracker.csumMtx.RLock() + checkSum := tracker.csum + tracker.csumMtx.RUnlock() + if !bytes.Equal(csum, checkSum) { t.Fatalf( "[handlePreimageRequest] csum was changed, exp: %s, got: %s", csum, - tracker.csum, + checkSum, ) } - tracker.mtx.RUnlock() }) t.Run("csum for cancel order", func(t *testing.T) { rig := newTestRig() @@ -4104,7 +4110,8 @@ func TestHandlePreimageRequest(t *testing.T) { epochLen: mkt.EpochLen, }, } - oid := tracker.cancel.ID() + oid := tracker.ID() + cid := tracker.cancel.ID() // Test the new path with rig.core.sentCommits. readyCommitment := func(commit order.Commitment) chan struct{} { @@ -4119,7 +4126,7 @@ func TestHandlePreimageRequest(t *testing.T) { commitCSum := dex.Bytes{2, 3, 5, 7, 11, 13} commitSig := readyCommitment(commit) payload := &msgjson.PreimageRequest{ - OrderID: oid[:], + OrderID: cid[:], Commitment: commit[:], CommitChecksum: commitCSum, } @@ -4127,7 +4134,8 @@ func TestHandlePreimageRequest(t *testing.T) { notes := rig.core.NotificationFeed() - rig.dc.trades[order.OrderID{}] = tracker + rig.dc.trades[oid] = tracker + rig.dc.registerCancelLink(cid, oid) err := handlePreimageRequest(rig.core, rig.dc, reqCommit) if err != nil { t.Fatalf("handlePreimageRequest error: %v", err) @@ -4146,15 +4154,17 @@ func TestHandlePreimageRequest(t *testing.T) { t.Fatal("no order note from preimage request handling") } - tracker.mtx.RLock() - if !bytes.Equal(commitCSum, tracker.cancel.csum) { + tracker.csumMtx.RLock() + cancelCsum := tracker.cancelCsum + tracker.csumMtx.RUnlock() + if !bytes.Equal(commitCSum, cancelCsum) { t.Fatalf( "handlePreimageRequest must initialize tracker cancel csum, exp: %s, got: %s", commitCSum, - tracker.cancel.csum, + cancelCsum, ) } - tracker.mtx.RUnlock() + }) t.Run("more than one preimage request for cancel order (different csums)", func(t *testing.T) { rig := newTestRig() @@ -4171,9 +4181,9 @@ func TestHandlePreimageRequest(t *testing.T) { db: rig.db, dc: rig.dc, metaData: &db.OrderMetaData{}, + // Simulate first preimage request by initializing csum here. + cancelCsum: firstCSum, cancel: &trackedCancel{ - // Simulate first preimage request by initializing csum here. - csum: firstCSum, CancelOrder: order.CancelOrder{ P: order.Prefix{ AccountID: rig.dc.acct.ID(), @@ -4188,7 +4198,8 @@ func TestHandlePreimageRequest(t *testing.T) { epochLen: mkt.EpochLen, }, } - oid := tracker.cancel.ID() + oid := tracker.ID() + cid := tracker.cancel.ID() // Test the new path with rig.core.sentCommits. readyCommitment := func(commit order.Commitment) chan struct{} { @@ -4203,7 +4214,7 @@ func TestHandlePreimageRequest(t *testing.T) { secondCSum := dex.Bytes{2, 3, 5, 7, 11, 14} commitSig := readyCommitment(commit) payload := &msgjson.PreimageRequest{ - OrderID: oid[:], + OrderID: cid[:], Commitment: commit[:], CommitChecksum: secondCSum, } @@ -4214,7 +4225,8 @@ func TestHandlePreimageRequest(t *testing.T) { rig.ws.sendMsgErrChan = make(chan *msgjson.Error, 1) defer func() { rig.ws.sendMsgErrChan = nil }() - rig.dc.trades[order.OrderID{}] = tracker + rig.dc.trades[oid] = tracker + rig.dc.registerCancelLink(cid, oid) err := handlePreimageRequest(rig.core, rig.dc, reqCommit) if err != nil { t.Fatalf("handlePreimageRequest error: %v", err) @@ -4232,15 +4244,16 @@ func TestHandlePreimageRequest(t *testing.T) { case <-time.After(time.Second): t.Fatal("no msgjson.Error sent from preimage request handling") } - tracker.mtx.RLock() - if !bytes.Equal(firstCSum, tracker.cancel.csum) { + tracker.csumMtx.RLock() + cancelCsum := tracker.cancelCsum + tracker.csumMtx.RUnlock() + if !bytes.Equal(firstCSum, cancelCsum) { t.Fatalf( "[handlePreimageRequest] cancel csum was changed, exp: %s, got: %s", firstCSum, - tracker.cancel.csum, + cancelCsum, ) } - tracker.mtx.RUnlock() }) t.Run("more than one preimage request for cancel order (same csum)", func(t *testing.T) { rig := newTestRig() @@ -4257,9 +4270,9 @@ func TestHandlePreimageRequest(t *testing.T) { db: rig.db, dc: rig.dc, metaData: &db.OrderMetaData{}, + // Simulate first preimage request by initializing csum here. + cancelCsum: csum, cancel: &trackedCancel{ - // Simulate first preimage request by initializing csum here. - csum: csum, CancelOrder: order.CancelOrder{ P: order.Prefix{ AccountID: rig.dc.acct.ID(), @@ -4274,7 +4287,8 @@ func TestHandlePreimageRequest(t *testing.T) { epochLen: mkt.EpochLen, }, } - oid := tracker.cancel.ID() + oid := tracker.ID() + cid := tracker.cancel.ID() // Test the new path with rig.core.sentCommits. readyCommitment := func(commit order.Commitment) chan struct{} { @@ -4288,7 +4302,7 @@ func TestHandlePreimageRequest(t *testing.T) { commit := preImg.Commit() commitSig := readyCommitment(commit) payload := &msgjson.PreimageRequest{ - OrderID: oid[:], + OrderID: cid[:], Commitment: commit[:], CommitChecksum: csum, } @@ -4296,7 +4310,8 @@ func TestHandlePreimageRequest(t *testing.T) { notes := rig.core.NotificationFeed() - rig.dc.trades[order.OrderID{}] = tracker + rig.dc.trades[oid] = tracker + rig.dc.registerCancelLink(cid, oid) err := handlePreimageRequest(rig.core, rig.dc, reqCommit) if err != nil { t.Fatalf("handlePreimageRequest error: %v", err) @@ -4315,15 +4330,16 @@ func TestHandlePreimageRequest(t *testing.T) { t.Fatal("no order note from preimage request handling") } - tracker.mtx.RLock() - if !bytes.Equal(csum, tracker.cancel.csum) { + tracker.csumMtx.RLock() + cancelCsum := tracker.cancelCsum + tracker.csumMtx.RUnlock() + if !bytes.Equal(csum, cancelCsum) { t.Fatalf( "[handlePreimageRequest] cancel csum was changed, exp: %s, got: %s", csum, - tracker.cancel.csum, + cancelCsum, ) } - tracker.mtx.RUnlock() }) } @@ -4391,6 +4407,7 @@ func TestHandleRevokeOrderMsg(t *testing.T) { tracker.cancel = &trackedCancel{CancelOrder: *co} coid := co.ID() rig.dc.trades[oid] = tracker + rig.dc.registerCancelLink(coid, oid) orderNotes, feedDone := orderNoteFeed(tCore) defer feedDone() @@ -5016,6 +5033,7 @@ func TestTradeTracking(t *testing.T) { } tracker.cancel = &trackedCancel{CancelOrder: *co, epochLen: mkt.EpochLen} coid := co.ID() + rig.dc.registerCancelLink(coid, tracker.ID()) m1 := &msgjson.Match{ OrderID: loid[:], MatchID: mid[:], @@ -7057,6 +7075,7 @@ func TestHandleNomatch(t *testing.T) { standingTracker.cancel = &trackedCancel{ CancelOrder: *cancelOrder, } + dc.registerCancelLink(cancelOID, standingOID) // 4. Market order. loWillBeMarket, dbOrder, preImgL, _ := makeLimitOrder(dc, true, dcrBtcLotSize*100, dcrBtcRateStep) @@ -7071,7 +7090,7 @@ func TestHandleNomatch(t *testing.T) { dc.trades[marketOID] = marketTracker runNomatch := func(tag string, oid order.OrderID) { - tracker, _, _ := dc.findOrder(oid) + tracker, _ := dc.findOrder(oid) if tracker == nil { t.Fatalf("%s: order ID not found", tag) } @@ -7084,7 +7103,7 @@ func TestHandleNomatch(t *testing.T) { } checkTradeStatus := func(tag string, oid order.OrderID, expStatus order.OrderStatus) { - tracker, _, _ := dc.findOrder(oid) + tracker, _ := dc.findOrder(oid) if tracker.metaData.Status != expStatus { t.Fatalf("%s: wrong status. expected %s, got %s", tag, expStatus, tracker.metaData.Status) } diff --git a/client/core/simnet_trade.go b/client/core/simnet_trade.go index e55e8c5b39..2e94f27341 100644 --- a/client/core/simnet_trade.go +++ b/client/core/simnet_trade.go @@ -2216,7 +2216,7 @@ func (client *simulationClient) findOrder(orderID string) (*trackedTrade, error) if err != nil { return nil, fmt.Errorf("error parsing order id %s -> %v", orderID, err) } - tracker, _, _ := client.dc().findOrder(oid) + tracker, _ := client.dc().findOrder(oid) return tracker, nil } diff --git a/client/core/trade.go b/client/core/trade.go index ef1fc0ccb9..fc3584caa5 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -213,8 +213,6 @@ func (m *matchTracker) token() string { // trackedCancel is always associated with a trackedTrade. type trackedCancel struct { order.CancelOrder - preImg order.Preimage - csum dex.Bytes // the commitment checksum provided in the preimage request epochLen uint64 matches struct { maker *msgjson.Match @@ -279,14 +277,18 @@ type trackedTrade struct { options map[string]string // metaData.Options (immutable) for Redeem and Swap redemptionReserves uint64 // metaData.RedemptionReserves (immutable) refundReserves uint64 // metaData.RefundReserves (immutable) + preImg order.Preimage + + csumMtx sync.RWMutex + csum dex.Bytes // the commitment checksum provided in the preimage request + cancelCsum dex.Bytes + cancelPreimg order.Preimage // mtx protects all read-write fields of the trackedTrade and the // matchTrackers in the matches map. mtx sync.RWMutex metaData *db.OrderMetaData wallets *walletSet - preImg order.Preimage - csum dex.Bytes // the commitment checksum provided in the preimage request coins map[string]asset.Coin coinsLocked bool change asset.Coin @@ -592,14 +594,18 @@ func (t *trackedTrade) cancelEpochIdx() uint64 { return uint64(t.cancel.Prefix().ServerTime.UnixMilli()) / epochLen } -func (t *trackedTrade) verifyCSum(csum dex.Bytes, epochIdx uint64) error { +func (t *trackedTrade) verifyCSum(vsum dex.Bytes, epochIdx uint64) error { t.mtx.RLock() defer t.mtx.RUnlock() + t.csumMtx.RLock() + csum, cancelCsum := t.csum, t.cancelCsum + t.csumMtx.RUnlock() + // First check the trade's recorded csum, if it is in this epoch. - if epochIdx == t.epochIdx() && !bytes.Equal(csum, t.csum) { + if epochIdx == t.epochIdx() && !bytes.Equal(vsum, csum) { return fmt.Errorf("checksum %s != trade order preimage request checksum %s for trade order %v", - csum, t.csum, t.ID()) + csum, csum, t.ID()) } if t.cancel == nil { @@ -607,9 +613,9 @@ func (t *trackedTrade) verifyCSum(csum dex.Bytes, epochIdx uint64) error { } // Check the linked cancel order if it is for this epoch. - if epochIdx == t.cancelEpochIdx() && !bytes.Equal(csum, t.cancel.csum) { + if epochIdx == t.cancelEpochIdx() && !bytes.Equal(vsum, cancelCsum) { return fmt.Errorf("checksum %s != cancel order preimage request checksum %s for cancel order %v", - csum, t.cancel.csum, t.cancel.ID()) + vsum, cancelCsum, t.cancel.ID()) } return nil // includes not in epoch @@ -731,19 +737,36 @@ func (t *trackedTrade) token() string { return (t.ID().String()) } +// clearCancel clears the unmatched cancel and deletes the cancel checksum and +// link to the trade in the dexConnection. clearCancel must be called with the +// trackedTrade.mtx locked. +func (t *trackedTrade) clearCancel(preImg order.Preimage) { + if t.cancel != nil { + t.dc.deleteCancelLink(t.cancel.ID()) + t.cancel = nil + } + t.csumMtx.Lock() + t.cancelCsum = nil + t.cancelPreimg = preImg + t.csumMtx.Unlock() +} + // cancelTrade sets the cancellation data with the order and its preimage. // cancelTrade must be called with the mtx write-locked. func (t *trackedTrade) cancelTrade(co *order.CancelOrder, preImg order.Preimage, epochLen uint64) error { + t.clearCancel(preImg) t.cancel = &trackedCancel{ CancelOrder: *co, - preImg: preImg, epochLen: epochLen, } - err := t.db.LinkOrder(t.ID(), co.ID()) + cid := co.ID() + oid := t.ID() + t.dc.registerCancelLink(cid, oid) + err := t.db.LinkOrder(oid, cid) if err != nil { - return fmt.Errorf("error linking cancel order %s for trade %s: %w", co.ID(), t.ID(), err) + return fmt.Errorf("error linking cancel order %s for trade %s: %w", cid, oid, err) } - t.metaData.LinkedOrder = co.ID() + t.metaData.LinkedOrder = cid return nil } @@ -766,7 +789,7 @@ func (t *trackedTrade) nomatch(oid order.OrderID) (assetMap, error) { t.dc.log.Errorf("DB error unlinking cancel order %s for trade %s: %v", oid, t.ID(), err) } // Clearing the trackedCancel allows this order to be canceled again. - t.cancel = nil + t.clearCancel(order.Preimage{}) t.metaData.LinkedOrder = order.OrderID{} subject, details := t.formatDetails(TopicMissedCancel, makeOrderToken(t.token())) @@ -1182,7 +1205,7 @@ func (t *trackedTrade) deleteCancelOrder() { t.dc.log.Errorf("Error updating status in db for cancel order %v to revoked: %v", cid, err) } // Unlink the cancel order from the trade. - t.cancel = nil + t.clearCancel(order.Preimage{}) t.metaData.LinkedOrder = order.OrderID{} // NOTE: caller may wish to update the trades's DB entry } From aee9af7489b33339047a83fea2473ef4949a112f Mon Sep 17 00:00:00 2001 From: buck54321 Date: Fri, 11 Oct 2024 07:23:07 -0500 Subject: [PATCH 3/7] binance: update book stream url (#3015) * update binance book stream url --- client/comms/wsconn.go | 25 +++++++++++++++++++------ client/core/core_test.go | 2 ++ client/mm/libxc/binance.go | 32 ++++++++++++++++++-------------- 3 files changed, 39 insertions(+), 20 deletions(-) diff --git a/client/comms/wsconn.go b/client/comms/wsconn.go index 96ffabd082..903aad8e59 100644 --- a/client/comms/wsconn.go +++ b/client/comms/wsconn.go @@ -98,6 +98,7 @@ type WsConn interface { RequestWithTimeout(msg *msgjson.Message, respHandler func(*msgjson.Message), expireTime time.Duration, expire func()) error Connect(ctx context.Context) (*sync.WaitGroup, error) MessageSource() <-chan *msgjson.Message + UpdateURL(string) } // When the DEX sends a request to the client, a responseHandler is created @@ -161,6 +162,7 @@ type wsConn struct { cfg *WsCfg tlsCfg *tls.Config readCh chan *msgjson.Message + urlV atomic.Value // string wsMtx sync.Mutex ws *websocket.Conn @@ -203,14 +205,25 @@ func NewWsConn(cfg *WsCfg) (WsConn, error) { ServerName: uri.Hostname(), } - return &wsConn{ + conn := &wsConn{ cfg: cfg, log: cfg.Logger, tlsCfg: tlsConfig, readCh: make(chan *msgjson.Message, readBuffSize), respHandlers: make(map[uint64]*responseHandler), reconnectCh: make(chan struct{}, 1), - }, nil + } + conn.urlV.Store(cfg.URL) + + return conn, nil +} + +func (conn *wsConn) UpdateURL(uri string) { + conn.urlV.Store(uri) +} + +func (conn *wsConn) url() string { + return conn.urlV.Load().(string) } // IsDown indicates if the connection is known to be down. @@ -240,7 +253,7 @@ func (conn *wsConn) connect(ctx context.Context) error { dialer.Proxy = http.ProxyFromEnvironment } - ws, _, err := dialer.DialContext(ctx, conn.cfg.URL, conn.cfg.ConnectHeaders) + ws, _, err := dialer.DialContext(ctx, conn.url(), conn.cfg.ConnectHeaders) if err != nil { if isErrorInvalidCert(err) { conn.setConnectionStatus(InvalidCert) @@ -331,7 +344,7 @@ func (conn *wsConn) handleReadError(err error) { var netErr net.Error if errors.As(err, &netErr) && netErr.Timeout() { - conn.log.Errorf("Read timeout on connection to %s.", conn.cfg.URL) + conn.log.Errorf("Read timeout on connection to %s.", conn.url()) reconnect() return } @@ -457,11 +470,11 @@ func (conn *wsConn) keepAlive(ctx context.Context) { return } - conn.log.Infof("Attempting to reconnect to %s...", conn.cfg.URL) + conn.log.Infof("Attempting to reconnect to %s...", conn.url()) err := conn.connect(ctx) if err != nil { conn.log.Errorf("Reconnect failed. Scheduling reconnect to %s in %.1f seconds.", - conn.cfg.URL, rcInt.Seconds()) + conn.url(), rcInt.Seconds()) time.AfterFunc(rcInt, func() { conn.reconnectCh <- struct{}{} }) diff --git a/client/core/core_test.go b/client/core/core_test.go index 95a73ddfb1..5162c80baf 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -347,6 +347,8 @@ func (conn *TWebsocket) Connect(context.Context) (*sync.WaitGroup, error) { return &sync.WaitGroup{}, conn.connectErr } +func (conn *TWebsocket) UpdateURL(string) {} + type TDB struct { updateWalletErr error acct *db.AccountInfo diff --git a/client/mm/libxc/binance.go b/client/mm/libxc/binance.go index de90cd72ea..dcd6a703b5 100644 --- a/client/mm/libxc/binance.go +++ b/client/mm/libxc/binance.go @@ -1618,7 +1618,7 @@ func (bnc *binance) getOrderbookSnapshot(ctx context.Context, mktSymbol string) // subscribeToAdditionalMarketDataStream is called when a new market is // subscribed to after the market data stream connection has already been // established. -func (bnc *binance) subscribeToAdditionalMarketDataStream(ctx context.Context, baseID, quoteID uint32) error { +func (bnc *binance) subscribeToAdditionalMarketDataStream(ctx context.Context, baseID, quoteID uint32) (err error) { baseCfg, quoteCfg, err := bncAssetCfgs(baseID, quoteID) if err != nil { return fmt.Errorf("error getting asset cfg for %d: %w", baseID, err) @@ -1627,6 +1627,10 @@ func (bnc *binance) subscribeToAdditionalMarketDataStream(ctx context.Context, b mktID := binanceMktID(baseCfg, quoteCfg) streamID := marketDataStreamID(mktID) + defer func() { + bnc.marketStream.UpdateURL(bnc.streamURL()) + }() + bnc.booksMtx.Lock() defer bnc.booksMtx.Unlock() @@ -1662,6 +1666,10 @@ func (bnc *binance) streams() []string { return streamNames } +func (bnc *binance) streamURL() string { + return fmt.Sprintf("%s/stream?streams=%s", bnc.wsURL, strings.Join(bnc.streams(), "/")) +} + // checkSubs will query binance for current market subscriptions and compare // that to what subscriptions we should have. If there is a discrepancy a // warning is logged and the market subbed or unsubbed. @@ -1756,8 +1764,7 @@ func (bnc *binance) connectToMarketDataStream(ctx context.Context, baseID, quote reconnectC := make(chan struct{}) checkSubsC := make(chan struct{}) - newConnection := func() (comms.WsConn, *dex.ConnectionMaster, error) { - addr := fmt.Sprintf("%s/stream?streams=%s", bnc.wsURL, strings.Join(bnc.streams(), "/")) + newConnection := func() (*dex.ConnectionMaster, error) { // Need to send key but not signature connectEventFunc := func(cs comms.ConnectionStatus) { if cs != comms.Disconnected && cs != comms.Connected { @@ -1776,7 +1783,7 @@ func (bnc *binance) connectToMarketDataStream(ctx context.Context, baseID, quote } } conn, err := comms.NewWsConn(&comms.WsCfg{ - URL: addr, + URL: bnc.streamURL(), // Binance Docs: The websocket server will send a ping frame every 3 // minutes. If the websocket server does not receive a pong frame // back from the connection within a 10 minute period, the connection @@ -1795,16 +1802,16 @@ func (bnc *binance) connectToMarketDataStream(ctx context.Context, baseID, quote RawHandler: bnc.handleMarketDataNote, }) if err != nil { - return nil, nil, err + return nil, err } bnc.marketStream = conn cm := dex.NewConnectionMaster(conn) if err = cm.ConnectOnce(ctx); err != nil { - return nil, nil, fmt.Errorf("websocketHandler remote connect: %v", err) + return nil, fmt.Errorf("websocketHandler remote connect: %v", err) } - return conn, cm, nil + return cm, nil } // Add the initial book to the books map @@ -1822,13 +1829,11 @@ func (bnc *binance) connectToMarketDataStream(ctx context.Context, baseID, quote bnc.booksMtx.Unlock() // Create initial connection to the market data stream - conn, cm, err := newConnection() + cm, err := newConnection() if err != nil { return fmt.Errorf("error connecting to market data stream : %v", err) } - bnc.marketStream = conn - book.sync(ctx) // Start a goroutine to reconnect every 12 hours @@ -1836,9 +1841,8 @@ func (bnc *binance) connectToMarketDataStream(ctx context.Context, baseID, quote reconnect := func() error { bnc.marketStreamMtx.Lock() defer bnc.marketStreamMtx.Unlock() - oldCm := cm - conn, cm, err = newConnection() + cm, err = newConnection() if err != nil { return err } @@ -1846,8 +1850,6 @@ func (bnc *binance) connectToMarketDataStream(ctx context.Context, baseID, quote if oldCm != nil { oldCm.Disconnect() } - - bnc.marketStream = conn return nil } @@ -1922,6 +1924,8 @@ func (bnc *binance) UnsubscribeMarket(baseID, quoteID uint32) (err error) { defer func() { bnc.booksMtx.Unlock() + conn.UpdateURL(bnc.streamURL()) + if closer != nil { closer.Disconnect() } From 7b24153c6d46a60b010c13e56c193a98fb30c25f Mon Sep 17 00:00:00 2001 From: Philemon Ukane Date: Mon, 14 Oct 2024 17:56:53 +0100 Subject: [PATCH 4/7] multi: add support for enabling/disabling a dex account (#2946) * multi: update AccountDisable method on core and db - Rename AccountDisable to ToggleAccountStatus and allow re-enabling a disabled account. --------- Signed-off-by: Philemon Ukane Co-authored-by: Brian Stafford --- client/core/account.go | 70 ++++++--- client/core/account_test.go | 64 +++++--- client/core/bond.go | 17 ++- client/core/core.go | 143 ++++++++++-------- client/core/core_test.go | 8 +- client/core/errors.go | 2 +- client/core/types.go | 15 ++ client/db/bolt/db.go | 113 +++++--------- client/db/bolt/db_test.go | 44 ++++-- client/db/interface.go | 5 +- client/db/types.go | 1 + client/webserver/api.go | 14 +- client/webserver/jsintl.go | 8 + client/webserver/live_test.go | 18 +-- client/webserver/locales/en-us.go | 3 +- .../webserver/site/src/html/dexsettings.tmpl | 30 ++-- client/webserver/site/src/js/dexsettings.ts | 49 ++++-- client/webserver/site/src/js/forms.ts | 4 +- client/webserver/site/src/js/locales.ts | 4 + client/webserver/site/src/js/markets.ts | 4 +- client/webserver/site/src/js/registry.ts | 1 + client/webserver/types.go | 7 +- client/webserver/webserver.go | 4 +- client/webserver/webserver_test.go | 2 +- 24 files changed, 372 insertions(+), 258 deletions(-) diff --git a/client/core/account.go b/client/core/account.go index 04b031c8d5..ce7760385f 100644 --- a/client/core/account.go +++ b/client/core/account.go @@ -14,9 +14,9 @@ import ( "github.com/decred/dcrd/dcrec/secp256k1/v4" ) -// disconnectDEX unsubscribes from the dex's orderbooks, ends the connection -// with the dex, and removes it from the connection map. -func (c *Core) disconnectDEX(dc *dexConnection) { +// stopDEXConnection unsubscribes from the dex's orderbooks and ends the +// connection with the dex. The dexConnection will still remain in c.conns map. +func (c *Core) stopDEXConnection(dc *dexConnection) { // Stop dexConnection books. dc.cfgMtx.RLock() if dc.cfg != nil { @@ -34,42 +34,68 @@ func (c *Core) disconnectDEX(dc *dexConnection) { } } dc.cfgMtx.RUnlock() + dc.connMaster.Disconnect() // disconnect +} + +// disconnectDEX disconnects a dex and removes it from the connection map. +func (c *Core) disconnectDEX(dc *dexConnection) { // Disconnect and delete connection from map. - dc.connMaster.Disconnect() + c.stopDEXConnection(dc) c.connMtx.Lock() delete(c.conns, dc.acct.host) c.connMtx.Unlock() } -// AccountDisable is used to disable an account by given host and application -// password. -func (c *Core) AccountDisable(pw []byte, addr string) error { +// ToggleAccountStatus is used to disable or enable an account by given host and +// application password. +func (c *Core) ToggleAccountStatus(pw []byte, host string, disable bool) error { // Validate password. - _, err := c.encryptionKey(pw) + crypter, err := c.encryptionKey(pw) if err != nil { return codedError(passwordErr, err) } - // Get dex connection by host. - dc, _, err := c.dex(addr) + // Get dex connection by host. All exchange servers (enabled or not) are loaded as + // dexConnections but disabled servers are not connected. + dc, _, err := c.dex(host) if err != nil { return newError(unknownDEXErr, "error retrieving dex conn: %w", err) } - // Check active orders or bonds. - if dc.hasActiveOrders() { - return fmt.Errorf("cannot disable account with active orders") + if dc.acct.isDisabled() == disable { + return nil // no-op } - if dc.hasUnspentBond() { - return fmt.Errorf("cannot disable account with unspent bonds") + + if disable { + // Check active orders or bonds. + if dc.hasActiveOrders() { + return fmt.Errorf("cannot disable account with active orders") + } + + if dc.hasUnspentBond() { + c.log.Info("Disabling dex server with unspent bonds. Bonds will be refunded when expired.") + } } - err = c.db.DisableAccount(dc.acct.host) + err = c.db.ToggleAccountStatus(host, disable) if err != nil { - return newError(accountDisableErr, "error disabling account: %w", err) + return newError(accountStatusUpdateErr, "error updating account status: %w", err) } - c.disconnectDEX(dc) + if disable { + dc.acct.toggleAccountStatus(true) + c.stopDEXConnection(dc) + } else { + acctInfo, err := c.db.Account(host) + if err != nil { + return err + } + dc, connected := c.connectAccount(acctInfo) + if !connected { + return fmt.Errorf("failed to connected re-enabled account: %w", err) + } + c.initializeDEXConnection(dc, crypter) + } return nil } @@ -188,7 +214,7 @@ func (c *Core) AccountImport(pw []byte, acct *Account, bonds []*db.Bond) error { return err } c.addDexConnection(dc) - c.initializeDEXConnections(crypter) + c.initializeDEXConnection(dc, crypter) return nil } @@ -255,7 +281,7 @@ func (c *Core) AccountImport(pw []byte, acct *Account, bonds []*db.Bond) error { return err } c.addDexConnection(dc) - c.initializeDEXConnections(crypter) + c.initializeDEXConnection(dc, crypter) return nil } @@ -368,9 +394,9 @@ func (c *Core) UpdateDEXHost(oldHost, newHost string, appPW []byte, certI any) ( } } - err = c.db.DisableAccount(oldDc.acct.host) + err = c.db.ToggleAccountStatus(oldDc.acct.host, true) if err != nil { - return nil, newError(accountDisableErr, "error disabling account: %w", err) + return nil, newError(accountStatusUpdateErr, "error updating account status: %w", err) } updatedHost = true diff --git a/client/core/account_test.go b/client/core/account_test.go index ddad6213ce..62f20bb4a6 100644 --- a/client/core/account_test.go +++ b/client/core/account_test.go @@ -59,32 +59,39 @@ func TestAccountExport(t *testing.T) { } */ -func TestAccountDisable(t *testing.T) { +func TestToggleAccountStatus(t *testing.T) { activeTrades := map[order.OrderID]*trackedTrade{ {}: {metaData: &db.OrderMetaData{Status: order.OrderStatusBooked}}, } tests := []struct { - name, host string - recryptErr, acctErr, disableAcctErr error - wantErr, wantErrCode, loseConns bool - activeTrades map[order.OrderID]*trackedTrade - errCode int + name, host string + recryptErr, acctErr, disableAcctErr error + wantErr, wantErrCode, loseConns, wantDisable bool + activeTrades map[order.OrderID]*trackedTrade + errCode int }{{ - name: "ok", - host: tDexHost, + name: "ok: disable account", + host: tDexHost, + wantDisable: true, }, { - name: "password error", - host: tDexHost, - recryptErr: tErr, - wantErr: true, - errCode: passwordErr, + name: "ok: enable account", + host: tDexHost, + wantDisable: false, + }, { + name: "password error", + host: tDexHost, + recryptErr: tErr, + wantErr: true, + errCode: passwordErr, + wantDisable: true, }, { name: "host error", host: ":bad:", wantErr: true, wantErrCode: true, errCode: unknownDEXErr, + wantDisable: true, }, { name: "dex not in conns", host: tDexHost, @@ -92,18 +99,21 @@ func TestAccountDisable(t *testing.T) { wantErr: true, wantErrCode: true, errCode: unknownDEXErr, + wantDisable: true, }, { name: "has active orders", host: tDexHost, activeTrades: activeTrades, wantErr: true, + wantDisable: true, }, { name: "disable account error", host: tDexHost, disableAcctErr: errors.New(""), wantErr: true, wantErrCode: true, - errCode: accountDisableErr, + errCode: accountStatusUpdateErr, + wantDisable: true, }} for _, test := range tests { @@ -122,7 +132,7 @@ func TestAccountDisable(t *testing.T) { } tCore.connMtx.Unlock() - err := tCore.AccountDisable(tPW, test.host) + err := tCore.ToggleAccountStatus(tPW, test.host, test.wantDisable) if test.wantErr { if err == nil { t.Fatalf("expected error for test %v", test.name) @@ -135,15 +145,21 @@ func TestAccountDisable(t *testing.T) { if err != nil { t.Fatalf("unexpected error for test %v: %v", test.name, err) } - if _, found := tCore.conns[test.host]; found { - t.Fatal("found disabled account dex connection") - } - if rig.db.disabledHost == nil { - t.Fatal("expected execution of db.DisableAccount") - } - if *rig.db.disabledHost != test.host { - t.Fatalf("expected db disabled account to match test host, want: %v"+ - " got: %v", test.host, *rig.db.disabledHost) + if test.wantDisable { + if dc, found := tCore.conns[test.host]; found && !dc.acct.isDisabled() { + t.Fatal("expected disabled dex account") + } + if rig.db.disabledHost == nil { + t.Fatal("expected a disable dex server host") + } + if *rig.db.disabledHost != test.host { + t.Fatalf("expected db account to match test host, want: %v"+ + " got: %v", test.host, *rig.db.disabledHost) + } + } else { + if dc, found := tCore.conns[test.host]; found && dc.acct.isDisabled() { + t.Fatal("expected enabled dex account") + } } } } diff --git a/client/core/bond.go b/client/core/bond.go index 291ee5b838..f6adc4b465 100644 --- a/client/core/bond.go +++ b/client/core/bond.go @@ -510,6 +510,13 @@ func (c *Core) refundExpiredBonds(ctx context.Context, acct *dexAccount, cfg *de // // TODO: if mustPost > 0 { wallet.RenewBond(...) } + // Ensure wallet is unlocked for use below. + _, err = wallet.refreshUnlock() + if err != nil { + c.log.Errorf("failed to unlock bond asset wallet %v: %v", unbip(state.BondAssetID), err) + continue + } + // Generate a refund tx paying to an address from the currently // connected wallet, using bond.KeyIndex to create the signed // transaction. The RefundTx is really a backup. @@ -705,7 +712,7 @@ func (c *Core) rotateBonds(ctx context.Context) { // locked. However, we must refund bonds regardless. bondCfg := c.dexBondConfig(dc, now) - if len(bondCfg.bondAssets) == 0 { + if len(bondCfg.bondAssets) == 0 && !dc.acct.isDisabled() { if !dc.IsDown() && dc.config() != nil { dc.log.Meter("no-bond-assets", time.Minute*10).Warnf("Zero bond assets reported for apparently connected DCRDEX server") } @@ -713,8 +720,6 @@ func (c *Core) rotateBonds(ctx context.Context) { } acctBondState := c.bondStateOfDEX(dc, bondCfg) - c.repostPendingBonds(dc, bondCfg, acctBondState, unlocked) - refundedAssets, expiredStrength, err := c.refundExpiredBonds(ctx, dc.acct, bondCfg, acctBondState, now) if err != nil { c.log.Errorf("Failed to refund expired bonds for %v: %v", dc.acct.host, err) @@ -724,6 +729,12 @@ func (c *Core) rotateBonds(ctx context.Context) { c.updateAssetBalance(assetID) } + if dc.acct.isDisabled() { + continue // For disabled account, we should only bother about unspent bonds that might have been refunded by refundExpiredBonds above. + } + + c.repostPendingBonds(dc, bondCfg, acctBondState, unlocked) + bondAsset := bondCfg.bondAssets[acctBondState.BondAssetID] if bondAsset == nil { if acctBondState.TargetTier > 0 { diff --git a/client/core/core.go b/client/core/core.go index e80ec38351..8db99944b5 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -478,6 +478,7 @@ func (c *Core) exchangeInfo(dc *dexConnection) *Exchange { Host: dc.acct.host, AcctID: acctID, ConnectionStatus: dc.status(), + Disabled: dc.acct.isDisabled(), } } @@ -516,6 +517,7 @@ func (c *Core) exchangeInfo(dc *dexConnection) *Exchange { Auth: acctBondState.ExchangeAuth, MaxScore: cfg.MaxScore, PenaltyThreshold: cfg.PenaltyThreshold, + Disabled: dc.acct.isDisabled(), } } @@ -5151,70 +5153,80 @@ func (c *Core) initializeDEXConnections(crypter encrypt.Crypter) { var wg sync.WaitGroup conns := c.dexConnections() for _, dc := range conns { - if dc.acct.isViewOnly() { - continue // don't attempt authDEX for view-only conn - } - - // Unlock before checking auth and continuing, because if the user - // logged out and didn't shut down, the account is still authed, but - // locked, and needs unlocked. - err := dc.acct.unlock(crypter) - if err != nil { - subject, details := c.formatDetails(TopicAccountUnlockError, dc.acct.host, err) - c.notify(newFeePaymentNote(TopicAccountUnlockError, subject, details, db.ErrorLevel, dc.acct.host)) // newDEXAuthNote? - continue - } + wg.Add(1) + go func(dc *dexConnection) { + defer wg.Done() + c.initializeDEXConnection(dc, crypter) + }(dc) + } - // Unlock the bond wallet if a target tier is set. - if bondAssetID, targetTier, maxBondedAmt := dc.bondOpts(); targetTier > 0 { - c.log.Debugf("Preparing %s wallet to maintain target tier of %d for %v, bonding limit %v", - unbip(bondAssetID), targetTier, dc.acct.host, maxBondedAmt) - wallet, exists := c.wallet(bondAssetID) - if !exists || !wallet.connected() { // connectWallets already run, just fail - subject, details := c.formatDetails(TopicBondWalletNotConnected, unbip(bondAssetID)) - var w *WalletState - if exists { - w = wallet.state() - } - c.notify(newWalletConfigNote(TopicBondWalletNotConnected, subject, details, db.ErrorLevel, w)) - } else if !wallet.unlocked() { - err = wallet.Unlock(crypter) - if err != nil { - subject, details := c.formatDetails(TopicWalletUnlockError, dc.acct.host, err) - c.notify(newFeePaymentNote(TopicWalletUnlockError, subject, details, db.ErrorLevel, dc.acct.host)) - } - } - } + wg.Wait() +} - if dc.acct.authed() { // should not be possible with newly idempotent login, but there's AccountImport... - continue // authDEX already done - } +// initializeDEXConnection connects to the DEX server in the conns map and +// authenticates the connection. +func (c *Core) initializeDEXConnection(dc *dexConnection, crypter encrypt.Crypter) { + if dc.acct.isViewOnly() { + return // don't attempt authDEX for view-only conn + } - // Pending bonds will be handled by authDEX. Expired bonds will be - // refunded by rotateBonds. + // Unlock before checking auth and continuing, because if the user + // logged out and didn't shut down, the account is still authed, but + // locked, and needs unlocked. + err := dc.acct.unlock(crypter) + if err != nil { + subject, details := c.formatDetails(TopicAccountUnlockError, dc.acct.host, err) + c.notify(newFeePaymentNote(TopicAccountUnlockError, subject, details, db.ErrorLevel, dc.acct.host)) // newDEXAuthNote? + return + } - // If the connection is down, authDEX will fail on Send. - if dc.IsDown() { - c.log.Warnf("Connection to %v not available for authorization. "+ - "It will automatically authorize when it connects.", dc.acct.host) - subject, details := c.formatDetails(TopicDEXDisconnected, dc.acct.host) - c.notify(newConnEventNote(TopicDEXDisconnected, subject, dc.acct.host, comms.Disconnected, details, db.ErrorLevel)) - continue - } + if dc.acct.isDisabled() { + return // For disabled account, we only want dc.acct.unlock above to initialize the account ID. + } - wg.Add(1) - go func(dc *dexConnection) { - defer wg.Done() - err := c.authDEX(dc) + // Unlock the bond wallet if a target tier is set. + if bondAssetID, targetTier, maxBondedAmt := dc.bondOpts(); targetTier > 0 { + c.log.Debugf("Preparing %s wallet to maintain target tier of %d for %v, bonding limit %v", + unbip(bondAssetID), targetTier, dc.acct.host, maxBondedAmt) + wallet, exists := c.wallet(bondAssetID) + if !exists || !wallet.connected() { // connectWallets already run, just fail + subject, details := c.formatDetails(TopicBondWalletNotConnected, unbip(bondAssetID)) + var w *WalletState + if exists { + w = wallet.state() + } + c.notify(newWalletConfigNote(TopicBondWalletNotConnected, subject, details, db.ErrorLevel, w)) + } else if !wallet.unlocked() { + err = wallet.Unlock(crypter) if err != nil { - subject, details := c.formatDetails(TopicDexAuthError, dc.acct.host, err) - c.notify(newDEXAuthNote(TopicDexAuthError, subject, dc.acct.host, false, details, db.ErrorLevel)) - return + subject, details := c.formatDetails(TopicWalletUnlockError, dc.acct.host, err) + c.notify(newFeePaymentNote(TopicWalletUnlockError, subject, details, db.ErrorLevel, dc.acct.host)) } - }(dc) + } } - wg.Wait() + if dc.acct.authed() { // should not be possible with newly idempotent login, but there's AccountImport... + return // authDEX already done + } + + // Pending bonds will be handled by authDEX. Expired bonds will be + // refunded by rotateBonds. + + // If the connection is down, authDEX will fail on Send. + if dc.IsDown() { + c.log.Warnf("Connection to %v not available for authorization. "+ + "It will automatically authorize when it connects.", dc.acct.host) + subject, details := c.formatDetails(TopicDEXDisconnected, dc.acct.host) + c.notify(newConnEventNote(TopicDEXDisconnected, subject, dc.acct.host, comms.Disconnected, details, db.ErrorLevel)) + return + } + + // Authenticate dex connection + err = c.authDEX(dc) + if err != nil { + subject, details := c.formatDetails(TopicDexAuthError, dc.acct.host, err) + c.notify(newDEXAuthNote(TopicDexAuthError, subject, dc.acct.host, false, details, db.ErrorLevel)) + } } // resolveActiveTrades loads order and match data from the database. Only active @@ -7143,7 +7155,7 @@ func (c *Core) initialize() error { wg.Add(1) go func(acct *db.AccountInfo) { defer wg.Done() - if c.connectAccount(acct) { + if _, connected := c.connectAccount(acct); connected { atomic.AddUint32(&liveConns, 1) } }(acct) @@ -7197,8 +7209,9 @@ func (c *Core) initialize() error { // connectAccount makes a connection to the DEX for the given account. If a // non-nil dexConnection is returned from newDEXConnection, it was inserted into // the conns map even if the connection attempt failed (connected == false), and -// the connect retry / keepalive loop is active. -func (c *Core) connectAccount(acct *db.AccountInfo) (connected bool) { +// the connect retry / keepalive loop is active. The intial connection attempt +// or keepalive loop will not run if acct is disabled. +func (c *Core) connectAccount(acct *db.AccountInfo) (dc *dexConnection, connected bool) { host, err := addrHost(acct.Host) if err != nil { c.log.Errorf("skipping loading of %s due to address parse error: %v", host, err) @@ -7207,7 +7220,7 @@ func (c *Core) connectAccount(acct *db.AccountInfo) (connected bool) { if c.cfg.TheOneHost != "" && c.cfg.TheOneHost != host { c.log.Infof("Running with --onehost = %q.", c.cfg.TheOneHost) - return false + return } var connectFlag connectDEXFlag @@ -7215,7 +7228,7 @@ func (c *Core) connectAccount(acct *db.AccountInfo) (connected bool) { connectFlag |= connectDEXFlagViewOnly } - dc, err := c.newDEXConnection(acct, connectFlag) + dc, err = c.newDEXConnection(acct, connectFlag) if err != nil { c.log.Errorf("Unable to prepare DEX %s: %v", host, err) return @@ -7228,7 +7241,7 @@ func (c *Core) connectAccount(acct *db.AccountInfo) (connected bool) { // Connected or not, the dexConnection goes in the conns map now. c.addDexConnection(dc) - return err == nil + return dc, err == nil } func (c *Core) dbOrders(host string) ([]*db.MetaOrder, error) { @@ -8193,7 +8206,7 @@ func (c *Core) startDexConnection(acctInfo *db.AccountInfo, dc *dexConnection) e // the dexConnection's ConnectionMaster is shut down. This goroutine should // be started as long as the reconnect loop is running. It only returns when // the wsConn is stopped. - listen := dc.broadcastingConnect() + listen := dc.broadcastingConnect() && !dc.acct.isDisabled() if listen { c.wg.Add(1) go c.listen(dc) @@ -8240,6 +8253,12 @@ func (c *Core) startDexConnection(acctInfo *db.AccountInfo, dc *dexConnection) e // according to ConnectResult.Bonds slice. } + if dc.acct.isDisabled() { + // Sort out the bonds with current time to indicate refundable bonds. + categorizeBonds(time.Now().Unix()) + return nil // nothing else to do + } + err := dc.connMaster.Connect(c.ctx) if err != nil { // Sort out the bonds with current time to indicate refundable bonds. diff --git a/client/core/core_test.go b/client/core/core_test.go index 5162c80baf..98e519caec 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -423,8 +423,12 @@ func (tdb *TDB) BondRefunded(host string, assetID uint32, bondCoinID []byte) err return nil } -func (tdb *TDB) DisableAccount(url string) error { - tdb.disabledHost = &url +func (tdb *TDB) ToggleAccountStatus(host string, disable bool) error { + if disable { + tdb.disabledHost = &host + } else { + tdb.disabledHost = nil + } return tdb.disableAccountErr } diff --git a/client/core/errors.go b/client/core/errors.go index f5de3a1f8a..1c30e850dc 100644 --- a/client/core/errors.go +++ b/client/core/errors.go @@ -42,7 +42,7 @@ const ( fileReadErr unknownDEXErr accountRetrieveErr - accountDisableErr + accountStatusUpdateErr suspendedAcctErr existenceCheckErr createWalletErr diff --git a/client/core/types.go b/client/core/types.go index 39e830836f..f05a9f806e 100644 --- a/client/core/types.go +++ b/client/core/types.go @@ -714,6 +714,7 @@ type Exchange struct { Auth ExchangeAuth `json:"auth"` PenaltyThreshold uint32 `json:"penaltyThreshold"` MaxScore uint32 `json:"maxScore"` + Disabled bool `json:"disabled"` } // newDisplayIDFromSymbols creates a display-friendly market ID for a base/quote @@ -817,6 +818,7 @@ type dexAccount struct { authMtx sync.RWMutex isAuthed bool + disabled bool pendingBondsConfs map[string]uint32 pendingBonds []*db.Bond // not yet confirmed bonds []*db.Bond // confirmed, and not yet expired @@ -835,6 +837,7 @@ func newDEXAccount(acctInfo *db.AccountInfo, viewOnly bool) *dexAccount { cert: acctInfo.Cert, dexPubKey: acctInfo.DEXPubKey, viewOnly: viewOnly, + disabled: acctInfo.Disabled, encKey: acctInfo.EncKey(), // privKey and id on decrypt pendingBondsConfs: make(map[string]uint32), // bonds are set separately when categorized in connectDEX @@ -958,6 +961,18 @@ func (a *dexAccount) status() (initialized, unlocked bool) { return len(a.encKey) > 0, a.privKey != nil } +func (a *dexAccount) isDisabled() bool { + a.authMtx.RLock() + defer a.authMtx.RUnlock() + return a.disabled +} + +func (a *dexAccount) toggleAccountStatus(disable bool) { + a.authMtx.Lock() + defer a.authMtx.Unlock() + a.disabled = disable +} + // locked will be true if the account private key is currently decrypted, or // there are no account keys generated yet. func (a *dexAccount) locked() bool { diff --git a/client/db/bolt/db.go b/client/db/bolt/db.go index 09130b986c..5e0b13c6b9 100644 --- a/client/db/bolt/db.go +++ b/client/db/bolt/db.go @@ -55,20 +55,19 @@ var ( // value encodings. var ( // bucket keys - appBucket = []byte("appBucket") - accountsBucket = []byte("accounts") - bondIndexesBucket = []byte("bondIndexes") - bondsSubBucket = []byte("bonds") // sub bucket of accounts - disabledAccountsBucket = []byte("disabledAccounts") - activeOrdersBucket = []byte("activeOrders") - archivedOrdersBucket = []byte("orders") - activeMatchesBucket = []byte("activeMatches") - archivedMatchesBucket = []byte("matches") - botProgramsBucket = []byte("botPrograms") - walletsBucket = []byte("wallets") - notesBucket = []byte("notes") - pokesBucket = []byte("pokes") - credentialsBucket = []byte("credentials") + appBucket = []byte("appBucket") + accountsBucket = []byte("accounts") + bondIndexesBucket = []byte("bondIndexes") + bondsSubBucket = []byte("bonds") // sub bucket of accounts + activeOrdersBucket = []byte("activeOrders") + archivedOrdersBucket = []byte("orders") + activeMatchesBucket = []byte("activeMatches") + archivedMatchesBucket = []byte("matches") + botProgramsBucket = []byte("botPrograms") + walletsBucket = []byte("wallets") + notesBucket = []byte("notes") + pokesBucket = []byte("pokes") + credentialsBucket = []byte("credentials") // value keys versionKey = []byte("version") @@ -179,7 +178,7 @@ func NewDB(dbPath string, logger dex.Logger, opts ...Opts) (dexdb.DB, error) { } if err = bdb.makeTopLevelBuckets([][]byte{ - appBucket, accountsBucket, bondIndexesBucket, disabledAccountsBucket, + appBucket, accountsBucket, bondIndexesBucket, activeOrdersBucket, archivedOrdersBucket, activeMatchesBucket, archivedMatchesBucket, walletsBucket, notesBucket, credentialsBucket, @@ -548,6 +547,8 @@ func loadAccountInfo(acct *bbolt.Bucket, log dex.Logger) (*db.AccountInfo, error return nil, err } + acctInfo.Disabled = bytes.Equal(acct.Get(activeKey), byteFalse) + bondsBkt := acct.Bucket(bondsSubBucket) if bondsBkt == nil { return acctInfo, nil // no bonds, OK for legacy account @@ -627,7 +628,7 @@ func (db *BoltDB) CreateAccount(ai *dexdb.AccountInfo) error { if err != nil { return fmt.Errorf("accountKey put error: %w", err) } - err = acct.Put(activeKey, byteTrue) // huh? + err = acct.Put(activeKey, byteTrue) if err != nil { return fmt.Errorf("activeKey put error: %w", err) } @@ -710,63 +711,31 @@ func (db *BoltDB) UpdateAccountInfo(ai *dexdb.AccountInfo) error { }) } -// deleteAccount removes the account by host. -func (db *BoltDB) deleteAccount(host string) error { - acctKey := []byte(host) +// ToggleAccountStatus enables or disables the account associated with the given +// host. +func (db *BoltDB) ToggleAccountStatus(host string, disable bool) error { return db.acctsUpdate(func(accts *bbolt.Bucket) error { - return accts.DeleteBucket(acctKey) - }) -} - -// DisableAccount disables the account associated with the given host -// and archives it. The Accounts and Account methods will no longer find -// the disabled account. -// -// TODO: Add disabledAccounts method for retrieval of a disabled account and -// possible recovery of the account data. -func (db *BoltDB) DisableAccount(url string) error { - // Get account's info. - ai, err := db.Account(url) - if err != nil { - return err - } - // Copy AccountInfo to disabledAccounts. Not necessary for view-only - // accounts. - acctKey := ai.EncKey() - if len(acctKey) > 0 { - err = db.disabledAcctsUpdate(func(disabledAccounts *bbolt.Bucket) error { - return disabledAccounts.Put(acctKey, ai.Encode()) - }) - if err != nil { - return err + acct := accts.Bucket([]byte(host)) + if acct == nil { + return fmt.Errorf("account not found for %s", host) } - } - // WARNING/TODO: account proof (fee paid info) not saved! - err = db.deleteAccount(ai.Host) - if err != nil { - if errors.Is(err, bbolt.ErrBucketNotFound) { - db.log.Warnf("Cannot delete account from active accounts"+ - " table. Host: not found. %s err: %v", ai.Host, err) - } else { - return err + + newStatus := byteTrue + if disable { + newStatus = byteFalse } - } - return nil -} -// disabledAccount gets the AccountInfo from disabledAccount associated with -// the specified EncKey. -func (db *BoltDB) disabledAccount(encKey []byte) (*dexdb.AccountInfo, error) { - var acctInfo *dexdb.AccountInfo - return acctInfo, db.disabledAcctsView(func(accts *bbolt.Bucket) error { - acct := accts.Get(encKey) - if acct == nil { - return fmt.Errorf("account not found for key") + if bytes.Equal(acct.Get(activeKey), newStatus) { + msg := "account is already enabled" + if disable { + msg = "account is already disabled" + } + return errors.New(msg) } - var err error - acctInfo, err = dexdb.DecodeAccountInfo(acct) + + err := acct.Put(activeKey, newStatus) if err != nil { - return err + return fmt.Errorf("accountKey put error: %w", err) } return nil }) @@ -782,16 +751,6 @@ func (db *BoltDB) acctsUpdate(f bucketFunc) error { return db.withBucket(accountsBucket, db.Update, f) } -// disabledAcctsView is a convenience function for reading from the disabledAccounts bucket. -func (db *BoltDB) disabledAcctsView(f bucketFunc) error { - return db.withBucket(disabledAccountsBucket, db.View, f) -} - -// disabledAcctsUpdate is a convenience function for inserting into the disabledAccounts bucket. -func (db *BoltDB) disabledAcctsUpdate(f bucketFunc) error { - return db.withBucket(disabledAccountsBucket, db.Update, f) -} - func (db *BoltDB) storeBond(bondBkt *bbolt.Bucket, bond *db.Bond) error { err := bondBkt.Put(bondKey, bond.Encode()) if err != nil { diff --git a/client/db/bolt/db_test.go b/client/db/bolt/db_test.go index f34d096fd2..341ee69d17 100644 --- a/client/db/bolt/db_test.go +++ b/client/db/bolt/db_test.go @@ -266,7 +266,7 @@ func TestAccounts(t *testing.T) { acct.DEXPubKey = dexKey } -func TestDisableAccount(t *testing.T) { +func TestToggleAccountStatus(t *testing.T) { boltdb, shutdown := newTestDB(t) defer shutdown() @@ -276,29 +276,43 @@ func TestDisableAccount(t *testing.T) { if err != nil { t.Fatalf("Unexpected CreateAccount error: %v", err) } - actualDisabledAccount, err := boltdb.disabledAccount(acct.EncKey()) - if err == nil { - t.Fatalf("Expected disabledAccount error but there was none.") + + accounts, err := boltdb.Accounts() + if err != nil { + t.Fatalf("Unexpected boltdb.Accounts error: %v", err) } - if actualDisabledAccount != nil { - t.Fatalf("Expected not to retrieve a disabledAccount.") + if len(accounts) != 1 { + t.Fatalf("Expected 1 account but got %d", len(accounts)) } - err = boltdb.DisableAccount(host) + // Test disable account + err = boltdb.ToggleAccountStatus(host, true) + if err != nil { + t.Fatalf("Unexpected ToggleAccountStatus error: %v", err) + } + actualAcct, err := boltdb.Account(host) if err != nil { - t.Fatalf("Unexpected DisableAccount error: %v", err) + t.Fatalf("Unexpected boltdb.Account error: %v", err) } - actualAcct, _ := boltdb.Account(host) - if actualAcct != nil { - t.Fatalf("Expected retrieval of deleted account to be nil") + + if !actualAcct.Disabled { + t.Fatalf("Expected a disabled account.") } - actualDisabledAccount, err = boltdb.disabledAccount(acct.EncKey()) + + // Test enable account + err = boltdb.ToggleAccountStatus(host, false) if err != nil { - t.Fatalf("Unexpected disabledAccount error: %v", err) + t.Fatalf("Unexpected ToggleAccountStatus error: %v", err) } - if actualDisabledAccount == nil { - t.Fatalf("Expected to retrieve a disabledAccount.") + + actualAcct, err = boltdb.Account(host) + if err != nil { + t.Fatalf("Unexpected boltdb.Account error: %v", err) + } + + if actualAcct.Disabled { + t.Fatalf("Expected an active account.") } } diff --git a/client/db/interface.go b/client/db/interface.go index beba5b8ec9..215de2dcec 100644 --- a/client/db/interface.go +++ b/client/db/interface.go @@ -47,8 +47,9 @@ type DB interface { ConfirmBond(host string, assetID uint32, bondCoinID []byte) error // BondRefunded records that a bond has been refunded. BondRefunded(host string, assetID uint32, bondCoinID []byte) error - // DisableAccount sets the AccountInfo disabled status to true. - DisableAccount(host string) error + // ToggleAccountStatus enables or disables the account associated with the + // given host. + ToggleAccountStatus(host string, disable bool) error // UpdateOrder saves the order information in the database. Any existing // order info will be overwritten without indication. UpdateOrder(m *MetaOrder) error diff --git a/client/db/types.go b/client/db/types.go index 05494c1121..d20bd4fae5 100644 --- a/client/db/types.go +++ b/client/db/types.go @@ -232,6 +232,7 @@ type AccountInfo struct { MaxBondedAmt uint64 PenaltyComps uint16 BondAsset uint32 // the asset to use when auto-posting bonds + Disabled bool // whether the account is disabled // DEPRECATED reg fee data. Bond txns are in a sub-bucket. // Left until we need to upgrade just for serialization simplicity. diff --git a/client/webserver/api.go b/client/webserver/api.go index a1aa701643..23bb0067fe 100644 --- a/client/webserver/api.go +++ b/client/webserver/api.go @@ -846,9 +846,9 @@ func (s *WebServer) apiRestoreWalletInfo(w http.ResponseWriter, r *http.Request) writeJSON(w, resp) } -// apiAccountDisable is the handler for the '/disableaccount' API request. -func (s *WebServer) apiAccountDisable(w http.ResponseWriter, r *http.Request) { - form := new(accountDisableForm) +// apiToggleAccountStatus is the handler for the '/toggleaccountstatus' API request. +func (s *WebServer) apiToggleAccountStatus(w http.ResponseWriter, r *http.Request) { + form := new(updateAccountStatusForm) defer form.Pass.Clear() if !readPost(w, r, form) { return @@ -860,12 +860,14 @@ func (s *WebServer) apiAccountDisable(w http.ResponseWriter, r *http.Request) { return } // Disable account. - err = s.core.AccountDisable(appPW, form.Host) + err = s.core.ToggleAccountStatus(appPW, form.Host, form.Disable) if err != nil { - s.writeAPIError(w, fmt.Errorf("error disabling account: %w", err)) + s.writeAPIError(w, fmt.Errorf("error updating account status: %w", err)) return } - w.Header().Set("Connection", "close") + if form.Disable { + w.Header().Set("Connection", "close") + } writeJSON(w, simpleAck()) } diff --git a/client/webserver/jsintl.go b/client/webserver/jsintl.go index cde67ece1f..733be59e2e 100644 --- a/client/webserver/jsintl.go +++ b/client/webserver/jsintl.go @@ -194,6 +194,10 @@ const ( archivedSettingsID = "ARCHIVED_SETTINGS" idTransparent = "TRANSPARENT" idNoCodeProvided = "NO_CODE_PROVIDED" + enableAccount = "ENABLE_ACCOUNT" + disableAccount = "DISABLE_ACCOUNT" + accountDisabledMsg = "ACCOUNT_DISABLED_MSG" + dexDisabledMsg = "DEX_DISABLED_MSG" ) var enUS = map[string]*intl.Translation{ @@ -387,6 +391,10 @@ var enUS = map[string]*intl.Translation{ archivedSettingsID: {T: "Archived Settings"}, idTransparent: {T: "Transparent"}, idNoCodeProvided: {T: "no code provided"}, + enableAccount: {T: "Enable Account"}, + disableAccount: {T: "Disable Account"}, + accountDisabledMsg: {T: "account disabled - re-enable to update settings"}, + dexDisabledMsg: {T: "DEX server is disabled. Visit the settings page to enable and connect to this server."}, } var ptBR = map[string]*intl.Translation{ diff --git a/client/webserver/live_test.go b/client/webserver/live_test.go index 66f012998f..cae2faf0c1 100644 --- a/client/webserver/live_test.go +++ b/client/webserver/live_test.go @@ -772,7 +772,7 @@ func (c *TCore) AccountExport(pw []byte, host string) (*core.Account, []*db.Bond func (c *TCore) AccountImport(pw []byte, account *core.Account, bond []*db.Bond) error { return nil } -func (c *TCore) AccountDisable(pw []byte, host string) error { return nil } +func (c *TCore) ToggleAccountStatus(pw []byte, host string, disable bool) error { return nil } func (c *TCore) TxHistory(assetID uint32, n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { return nil, nil @@ -2176,13 +2176,13 @@ func (m *TMarketMaker) StartBot(startCfg *mm.StartConfig, alternateConfigPath *s mkt.BaseID: randomBalance(), }, DEXBalances: map[uint32]*mm.BotBalance{ - mkt.BaseID: &mm.BotBalance{ + mkt.BaseID: { Available: randomBalance(), Locked: randomBalance(), Pending: randomBalance(), Reserved: randomBalance(), }, - mkt.BaseID: &mm.BotBalance{ + mkt.BaseID: { Available: randomBalance(), Locked: randomBalance(), Pending: randomBalance(), @@ -2190,13 +2190,13 @@ func (m *TMarketMaker) StartBot(startCfg *mm.StartConfig, alternateConfigPath *s }, }, CEXBalances: map[uint32]*mm.BotBalance{ - mkt.BaseID: &mm.BotBalance{ + mkt.BaseID: { Available: randomBalance(), Locked: randomBalance(), Pending: randomBalance(), Reserved: randomBalance(), }, - mkt.BaseID: &mm.BotBalance{ + mkt.BaseID: { Available: randomBalance(), Locked: randomBalance(), Pending: randomBalance(), @@ -2313,12 +2313,12 @@ func (m *TMarketMaker) Status() *mm.Status { stats = &mm.RunStats{ InitialBalances: make(map[uint32]uint64), DEXBalances: map[uint32]*mm.BotBalance{ - botCfg.BaseID: &mm.BotBalance{Available: randomBalance()}, - botCfg.QuoteID: &mm.BotBalance{Available: randomBalance()}, + botCfg.BaseID: {Available: randomBalance()}, + botCfg.QuoteID: {Available: randomBalance()}, }, CEXBalances: map[uint32]*mm.BotBalance{ - botCfg.BaseID: &mm.BotBalance{Available: randomBalance()}, - botCfg.QuoteID: &mm.BotBalance{Available: randomBalance()}, + botCfg.BaseID: {Available: randomBalance()}, + botCfg.QuoteID: {Available: randomBalance()}, }, ProfitLoss: randomProfitLoss(botCfg.BaseID, botCfg.QuoteID), StartTime: time.Now().Add(-time.Duration(float64(time.Hour*10) * rand.Float64())).Unix(), diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index cff6a9089e..841795abb2 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -51,7 +51,7 @@ var EnUS = map[string]*intl.Translation{ "Authorize Export": {T: "Authorize Export"}, "export_app_pw_msg": {T: "Enter your app password to confirm account export for"}, "Disable Account": {T: "Disable Account"}, - "disable_dex_server": {T: "This DEX server may be re-enabled at any time in the future by adding it again."}, + "disable_dex_server": {T: "This DEX server may be re-enabled at any time in the future on the settings page.", Version: 1}, "Authorize Import": {T: "Authorize Import"}, "app_pw_import_msg": {T: "Enter your app password to confirm account import"}, "Account File": {T: "Account File"}, @@ -653,4 +653,5 @@ var EnUS = map[string]*intl.Translation{ "Transaction": {T: "Transaction"}, "Value": {T: "Value"}, "Prepaid bond redeemed": {T: "Prepaid bond redeemed!"}, + "Enable Account": {T: "Enable Account"}, } diff --git a/client/webserver/site/src/html/dexsettings.tmpl b/client/webserver/site/src/html/dexsettings.tmpl index 55fd19c50b..acb38bc92d 100644 --- a/client/webserver/site/src/html/dexsettings.tmpl +++ b/client/webserver/site/src/html/dexsettings.tmpl @@ -1,6 +1,6 @@ {{define "dexsettings"}} {{template "top" .}} -
+
@@ -21,11 +21,13 @@
- [[[target_tier]]] + [[[target_tier]]]
- [[[Actual Tier]]] + [[[Actual Tier]]]
@@ -39,7 +41,7 @@
- +
@@ -47,7 +49,7 @@
Auto Renew
-
+
@@ -61,7 +63,7 @@
- +
@@ -73,18 +75,24 @@
+
+ +
-
- -
-
+
[[[successful_cert_update]]]
-
+
diff --git a/client/webserver/site/src/js/dexsettings.ts b/client/webserver/site/src/js/dexsettings.ts index 0fa3d0efa4..43f4f226cc 100644 --- a/client/webserver/site/src/js/dexsettings.ts +++ b/client/webserver/site/src/js/dexsettings.ts @@ -32,6 +32,7 @@ export default class DexSettingsPage extends BasePage { currentForm: PageElement page: Record host: string + accountDisabled:boolean keyup: (e: KeyboardEvent) => void dexAddrForm: forms.DEXAddressForm bondFeeBufferCache: Record @@ -101,7 +102,12 @@ export default class DexSettingsPage extends BasePage { this.reputationMeter.setHost(host) Doc.bind(page.exportDexBtn, 'click', () => this.exportAccount()) - Doc.bind(page.disableAcctBtn, 'click', () => this.prepareAccountDisable(page.disableAccountForm)) + + this.accountDisabled = body.dataset.disabled === 'true' + Doc.bind(page.toggleAccountStatusBtn, 'click', () => { + if (!this.accountDisabled) this.prepareAccountDisable(page.disableAccountForm) + else this.toggleAccountStatus(false) + }) Doc.bind(page.updateCertBtn, 'click', () => page.certFileInput.click()) Doc.bind(page.updateHostBtn, 'click', () => this.prepareUpdateHost()) Doc.bind(page.certFileInput, 'change', () => this.onCertFileChange()) @@ -114,12 +120,13 @@ export default class DexSettingsPage extends BasePage { Doc.bind(page.changeTier, 'click', () => { showTierForm() }) const willAutoRenew = xc.auth.targetTier > 0 this.renewToggle = new AniToggle(page.toggleAutoRenew, page.renewErr, willAutoRenew, async (newState: boolean) => { + if (this.accountDisabled) return if (newState) showTierForm() else return this.disableAutoRenew() }) Doc.bind(page.autoRenewBox, 'click', (e: MouseEvent) => { e.stopPropagation() - page.toggleAutoRenew.click() + if (!this.accountDisabled) page.toggleAutoRenew.click() }) page.penaltyComps.textContent = String(xc.auth.penaltyComps) @@ -169,11 +176,11 @@ export default class DexSettingsPage extends BasePage { }) this.dexAddrForm = new forms.DEXAddressForm(page.dexAddrForm, async (xc: Exchange) => { - window.location.assign(`/dexsettings/${xc.host}`) + app().loadPage(`/dexsettings/${xc.host}`) }, this.host) // forms.bind(page.bondDetailsForm, page.updateBondOptionsConfirm, () => this.updateBondOptions()) - forms.bind(page.disableAccountForm, page.disableAccountConfirm, () => this.disableAccount()) + forms.bind(page.disableAccountForm, page.disableAccountConfirm, () => this.toggleAccountStatus(true)) Doc.bind(page.forms, 'mousedown', (e: MouseEvent) => { if (!Doc.mouseInElement(e, this.currentForm)) { this.closePopups() } @@ -321,21 +328,34 @@ export default class DexSettingsPage extends BasePage { Doc.hide(page.forms) } - // disableAccount disables the account associated with the provided host. - async disableAccount () { + // toggleAccountStatus enables or disables the account associated with the + // provided host. + async toggleAccountStatus (disable:boolean) { const page = this.page - const host = page.disableAccountHost.textContent - const req = { host } + Doc.hide(page.errMsg) + let host: string|null = this.host + if (disable) host = page.disableAccountHost.textContent + const req = { host, disable: disable } const loaded = app().loading(this.body) - const res = await postJSON('/api/disableaccount', req) + const res = await postJSON('/api/toggleaccountstatus', req) loaded() if (!app().checkResponse(res)) { - page.disableAccountErr.textContent = res.msg - Doc.show(page.disableAccountErr) + if (disable) { + page.disableAccountErr.textContent = res.msg + Doc.show(page.disableAccountErr) + } else { + page.errMsg.textContent = res.msg + Doc.show(page.errMsg) + } return } - Doc.hide(page.forms) - window.location.assign('/settings') + if (disable) { + this.page.toggleAccountStatusBtn.textContent = intl.prep(intl.ID_ENABLE_ACCOUNT) + Doc.hide(page.forms) + } else this.page.toggleAccountStatusBtn.textContent = intl.prep(intl.ID_DISABLE_ACCOUNT) + + this.accountDisabled = disable + app().loadPage(`dexsettings/${host}`) } async prepareAccountDisable (disableAccountForm: HTMLElement) { @@ -402,7 +422,8 @@ export default class DexSettingsPage extends BasePage { break case ConnectionStatus.Disconnected: displayIcons(false) - page.connectionStatus.textContent = intl.prep(intl.ID_DISCONNECTED) + if (this.accountDisabled) page.connectionStatus.textContent = intl.prep(intl.ID_ACCOUNT_DISABLED_MSG) + else page.connectionStatus.textContent = intl.prep(intl.ID_DISCONNECTED) break case ConnectionStatus.InvalidCert: displayIcons(false) diff --git a/client/webserver/site/src/js/forms.ts b/client/webserver/site/src/js/forms.ts index 30f53e2213..e253d9cec5 100644 --- a/client/webserver/site/src/js/forms.ts +++ b/client/webserver/site/src/js/forms.ts @@ -1037,13 +1037,13 @@ export class FeeAssetSelectionForm { this.marketRows.push({ mkt, tmpl, setTier }) } - for (const { symbol, id: assetID } of Object.values(xc.assets)) { + for (const { symbol, id: assetID } of Object.values(xc.assets || {})) { if (!app().assets[assetID]) continue const bondAsset = xc.bondAssets[symbol] if (bondAsset) addBondRow(assetID, bondAsset) } - for (const mkt of Object.values(xc.markets)) addMarketRow(mkt) + for (const mkt of Object.values(xc.markets || {})) addMarketRow(mkt) // page.host.textContent = xc.host page.tradingTierInput.value = xc.auth.targetTier ? String(xc.auth.targetTier) : '1' diff --git a/client/webserver/site/src/js/locales.ts b/client/webserver/site/src/js/locales.ts index a84e0cf228..1dd2ee7ddf 100644 --- a/client/webserver/site/src/js/locales.ts +++ b/client/webserver/site/src/js/locales.ts @@ -194,6 +194,10 @@ export const ID_PENDING = 'PENDING' export const ID_COMPLETE = 'COMPLETE' export const ID_ARCHIVED_SETTINGS = 'ARCHIVED_SETTINGS' export const ID_NO_CODE_PROVIDED = 'NO_CODE_PROVIDED' +export const ID_ENABLE_ACCOUNT = 'ENABLE_ACCOUNT' +export const ID_DISABLE_ACCOUNT = 'DISABLE_ACCOUNT' +export const ID_ACCOUNT_DISABLED_MSG = 'ACCOUNT_DISABLED_MSG' +export const ID_DEX_DISABLED_MSG = 'DEX_DISABLED_MSG' let locale: Locale diff --git a/client/webserver/site/src/js/markets.ts b/client/webserver/site/src/js/markets.ts index 9c196d6639..9ed605ac0c 100644 --- a/client/webserver/site/src/js/markets.ts +++ b/client/webserver/site/src/js/markets.ts @@ -1106,7 +1106,9 @@ export default class MarketsPage extends BasePage { // exchange data, so just put up a message and wait for the connection to be // established, at which time handleConnNote will refresh and reload. if (!dex || !dex.markets || dex.connectionStatus !== ConnectionStatus.Connected) { - page.chartErrMsg.textContent = intl.prep(intl.ID_CONNECTION_FAILED) + let errMsg = intl.prep(intl.ID_CONNECTION_FAILED) + if (dex.disabled) errMsg = intl.prep(intl.ID_DEX_DISABLED_MSG) + page.chartErrMsg.textContent = errMsg Doc.show(page.chartErrMsg) return } diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts index 5e8c088bf2..98ad0c3a4c 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -63,6 +63,7 @@ export interface Exchange { candleDurs: string[] maxScore: number penaltyThreshold: number + disabled:boolean } export interface Candle { diff --git a/client/webserver/types.go b/client/webserver/types.go index 6ec08206ab..f5b8e48be4 100644 --- a/client/webserver/types.go +++ b/client/webserver/types.go @@ -133,9 +133,10 @@ type accountImportForm struct { Bonds []*db.Bond `json:"bonds"` } -type accountDisableForm struct { - Pass encode.PassBytes `json:"pw"` - Host string `json:"host"` +type updateAccountStatusForm struct { + Pass encode.PassBytes `json:"pw"` + Host string `json:"host"` + Disable bool `json:"disable"` } type deleteRecordsForm struct { diff --git a/client/webserver/webserver.go b/client/webserver/webserver.go index 7a44b1da68..1ea09fa6fe 100644 --- a/client/webserver/webserver.go +++ b/client/webserver/webserver.go @@ -133,7 +133,7 @@ type clientCore interface { MaxSell(host string, base, quote uint32) (*core.MaxOrderEstimate, error) AccountExport(pw []byte, host string) (*core.Account, []*db.Bond, error) AccountImport(pw []byte, account *core.Account, bonds []*db.Bond) error - AccountDisable(pw []byte, host string) error + ToggleAccountStatus(pw []byte, host string, disable bool) error IsInitialized() bool ExportSeed(pw []byte) (string, error) PreOrder(*core.TradeForm) (*core.OrderEstimate, error) @@ -536,7 +536,7 @@ func New(cfg *Config) (*WebServer, error) { apiAuth.Post("/exportaccount", s.apiAccountExport) apiAuth.Post("/exportseed", s.apiExportSeed) apiAuth.Post("/importaccount", s.apiAccountImport) - apiAuth.Post("/disableaccount", s.apiAccountDisable) + apiAuth.Post("/toggleaccountstatus", s.apiToggleAccountStatus) apiAuth.Post("/accelerateorder", s.apiAccelerateOrder) apiAuth.Post("/preaccelerate", s.apiPreAccelerate) apiAuth.Post("/accelerationestimate", s.apiAccelerationEstimate) diff --git a/client/webserver/webserver_test.go b/client/webserver/webserver_test.go index 4bfb500780..40cbf0d52c 100644 --- a/client/webserver/webserver_test.go +++ b/client/webserver/webserver_test.go @@ -246,7 +246,7 @@ func (c *TCore) AccountExport(pw []byte, host string) (*core.Account, []*db.Bond func (c *TCore) AccountImport(pw []byte, account *core.Account, bonds []*db.Bond) error { return nil } -func (c *TCore) AccountDisable(pw []byte, host string) error { return nil } +func (c *TCore) ToggleAccountStatus(pw []byte, host string, disable bool) error { return nil } func (c *TCore) ExportSeed(pw []byte) (string, error) { return "seed words here", nil From 6420232acad34da9b7fbe756e96c323dcefd6b84 Mon Sep 17 00:00:00 2001 From: dev-warrior777 <126670673+dev-warrior777@users.noreply.github.com> Date: Tue, 15 Oct 2024 04:30:29 +0800 Subject: [PATCH 5/7] client/wiki: Update minimum Firo version to 0.14.14.0 (#3002) Update for consensus change hard fork Co-authored-by: dev-warrior777 --- client/asset/firo/firo.go | 4 ++-- docs/wiki/Client-Installation-and-Configuration.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/asset/firo/firo.go b/client/asset/firo/firo.go index ebfc036055..b2349da9f0 100644 --- a/client/asset/firo/firo.go +++ b/client/asset/firo/firo.go @@ -28,8 +28,8 @@ const ( version = 0 // Zcoin XZC BipID = 136 - // Lelantus Spark. Net proto 90031. Wallet version 130000 - minNetworkVersion = 141303 + // Consensus changes v0.14.14.0 + minNetworkVersion = 141400 walletTypeRPC = "firodRPC" walletTypeElectrum = "electrumRPC" estimateFeeConfs = 2 // 2 blocks should be enough diff --git a/docs/wiki/Client-Installation-and-Configuration.md b/docs/wiki/Client-Installation-and-Configuration.md index c95b90bb94..716f32d122 100644 --- a/docs/wiki/Client-Installation-and-Configuration.md +++ b/docs/wiki/Client-Installation-and-Configuration.md @@ -56,7 +56,7 @@ checkmark in the "native" column, no external software is required. | Dogecoin | x | [v1.14.7.0](https://dogecoin.com/) | x | | | Zcash | x | [v5.4.2](https://z.cash/download/) | x | | | Dash | x | [v20.1.1](https://github.com/dashpay/dash/releases) | x | | -| Firo | x | [v0.14.13.3](https://github.com/firoorg/firo/releases) | [v4.1.5.5](https://github.com/firoorg/electrum-firo/releases) | | +| Firo | x | [v0.14.14.0](https://github.com/firoorg/firo/releases) | [v4.1.5.5](https://github.com/firoorg/electrum-firo/releases) | | NOTE: The Electrum option is less mature and provides less privacy than the other wallet types. Some manual configuration of the Electrum wallet's RPC From 2e70a1fd9db098f4182e0d3e38ec5f023d077cb6 Mon Sep 17 00:00:00 2001 From: buck54321 Date: Tue, 15 Oct 2024 08:50:02 -0500 Subject: [PATCH 6/7] use ubuntu 22 for github actions (#3024) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d9e602f451..5fca52d211 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ permissions: jobs: build-go: name: Go CI - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: go: ['1.22', '1.23'] From 139cebe080160c5836d74d01f3f55206b4ff2b19 Mon Sep 17 00:00:00 2001 From: buck54321 Date: Tue, 15 Oct 2024 09:05:30 -0500 Subject: [PATCH 7/7] remove unused field from fundrawtransaction result struct (#3021) --- client/asset/btc/rpcclient.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/asset/btc/rpcclient.go b/client/asset/btc/rpcclient.go index 0e42be865a..5a420a0472 100644 --- a/client/asset/btc/rpcclient.go +++ b/client/asset/btc/rpcclient.go @@ -907,9 +907,8 @@ func (wc *rpcClient) estimateSendTxFee(tx *wire.MsgTx, feeRate uint64, subtract args = append(args, options) var res struct { - TxBytes dex.Bytes `json:"hex"` - Fees float64 `json:"fee"` - ChangePosition uint32 `json:"changepos"` + TxBytes dex.Bytes `json:"hex"` + Fees float64 `json:"fee"` } err = wc.call(methodFundRawTransaction, args, &res) if err != nil {