From 11883cb67979dd1284349f8332420ce6d70d37d9 Mon Sep 17 00:00:00 2001 From: Philemon Ukane Date: Wed, 4 Sep 2024 09:44:41 +0100 Subject: [PATCH] multi: allow enabling and disabling of dex account Signed-off-by: Philemon Ukane --- client/core/account.go | 38 +++++++++------ client/core/bond.go | 8 +++- client/core/core.go | 23 ++++++---- client/core/types.go | 15 ++++++ client/db/bolt/db.go | 2 +- client/db/bolt/db_test.go | 4 +- client/db/types.go | 2 +- client/webserver/jsintl.go | 8 ++++ client/webserver/locales/en-us.go | 3 +- .../webserver/site/src/html/dexsettings.tmpl | 32 ++++++++----- client/webserver/site/src/js/dexsettings.ts | 46 ++++++++++++++----- client/webserver/site/src/js/locales.ts | 4 ++ client/webserver/site/src/js/markets.ts | 4 +- client/webserver/site/src/js/registry.ts | 1 + 14 files changed, 136 insertions(+), 54 deletions(-) diff --git a/client/core/account.go b/client/core/account.go index 9e60f969a4..c604c95c9b 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,8 +34,13 @@ 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() @@ -45,19 +50,19 @@ func (c *Core) disconnectDEX(dc *dexConnection) { // application password. func (c *Core) ToggleAccountStatus(pw []byte, addr string, disable bool) error { // Validate password. - _, err := c.encryptionKey(pw) + crypter, err := c.encryptionKey(pw) if err != nil { return codedError(passwordErr, err) } - var dc *dexConnection - if disable { - // Get dex connection by host. - dc, _, err = c.dex(addr) - if err != nil { - return newError(unknownDEXErr, "error retrieving dex conn: %w", err) - } + // 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(addr) + if err != nil { + return newError(unknownDEXErr, "error retrieving dex conn: %w", err) + } + if disable { // Check active orders or bonds. if dc.hasActiveOrders() { return fmt.Errorf("cannot disable account with active orders") @@ -70,14 +75,19 @@ func (c *Core) ToggleAccountStatus(pw []byte, addr string, disable bool) error { } if disable { - c.disconnectDEX(dc) + dc.acct.toggleAccountStatus(true) + c.stopDEXConnection(dc) } else { acct, err := c.db.Account(addr) if err != nil { return err } - c.connectAccount(acct) + if !c.connectAccount(acct) { + c.log.Errorf("Failed to establish connection to %s (will retry)", addr) + } + + c.initializeDEXConnections(crypter) } return nil diff --git a/client/core/bond.go b/client/core/bond.go index 291ee5b838..b544d2c5ec 100644 --- a/client/core/bond.go +++ b/client/core/bond.go @@ -713,8 +713,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 +722,12 @@ func (c *Core) rotateBonds(ctx context.Context) { c.updateAssetBalance(assetID) } + if dc.acct.isDisabled() { + continue // we can only attempt bond refund(if any) for disabled accounts + } + + 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 2af05c5e02..d5635c8a8f 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -455,6 +455,7 @@ func (c *Core) exchangeInfo(dc *dexConnection) *Exchange { Host: dc.acct.host, AcctID: acctID, ConnectionStatus: dc.status(), + Disabled: dc.acct.isDisabled(), } } @@ -493,6 +494,7 @@ func (c *Core) exchangeInfo(dc *dexConnection) *Exchange { Auth: acctBondState.ExchangeAuth, MaxScore: cfg.MaxScore, PenaltyThreshold: cfg.PenaltyThreshold, + Disabled: dc.acct.isDisabled(), } } @@ -5138,6 +5140,10 @@ func (c *Core) initializeDEXConnections(crypter encrypt.Crypter) { continue } + if dc.acct.isDisabled() { + continue // we can only unlock the dex account to init the account ID. + } + // 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", @@ -7113,12 +7119,6 @@ func (c *Core) initialize() error { var liveConns uint32 var wg sync.WaitGroup for _, acct := range accts { - if !acct.Active { - // TODO: We should list this account separatly for display on the - // dex settings page to allow re-enabling this server. But we should - // listen for unspent bond refund if any. - continue - } wg.Add(1) go func(acct *db.AccountInfo) { defer wg.Done() @@ -7172,7 +7172,8 @@ 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. +// 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) (connected bool) { host, err := addrHost(acct.Host) if err != nil { @@ -8156,7 +8157,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) @@ -8203,6 +8204,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/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 a1dd34986b..5e0b13c6b9 100644 --- a/client/db/bolt/db.go +++ b/client/db/bolt/db.go @@ -547,7 +547,7 @@ func loadAccountInfo(acct *bbolt.Bucket, log dex.Logger) (*db.AccountInfo, error return nil, err } - acctInfo.Active = bytes.Equal(acct.Get(activeKey), byteTrue) + acctInfo.Disabled = bytes.Equal(acct.Get(activeKey), byteFalse) bondsBkt := acct.Bucket(bondsSubBucket) if bondsBkt == nil { diff --git a/client/db/bolt/db_test.go b/client/db/bolt/db_test.go index 35c48039b1..341ee69d17 100644 --- a/client/db/bolt/db_test.go +++ b/client/db/bolt/db_test.go @@ -296,7 +296,7 @@ func TestToggleAccountStatus(t *testing.T) { t.Fatalf("Unexpected boltdb.Account error: %v", err) } - if actualAcct.Active { + if !actualAcct.Disabled { t.Fatalf("Expected a disabled account.") } @@ -311,7 +311,7 @@ func TestToggleAccountStatus(t *testing.T) { t.Fatalf("Unexpected boltdb.Account error: %v", err) } - if !actualAcct.Active { + if actualAcct.Disabled { t.Fatalf("Expected an active account.") } } diff --git a/client/db/types.go b/client/db/types.go index e23c1b6cc0..d20bd4fae5 100644 --- a/client/db/types.go +++ b/client/db/types.go @@ -232,7 +232,7 @@ type AccountInfo struct { MaxBondedAmt uint64 PenaltyComps uint16 BondAsset uint32 // the asset to use when auto-posting bonds - Active bool // whether the account is enabled + 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/jsintl.go b/client/webserver/jsintl.go index 2f7fa5d5a9..4cb29c25ef 100644 --- a/client/webserver/jsintl.go +++ b/client/webserver/jsintl.go @@ -195,6 +195,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{ @@ -389,6 +393,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/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..36eb61640f 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]]]
-
+
@@ -144,4 +152,4 @@
{{template "bottom"}} -{{end}} +{{end}} \ No newline at end of file diff --git a/client/webserver/site/src/js/dexsettings.ts b/client/webserver/site/src/js/dexsettings.ts index a23839a7cc..5936441d8f 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) @@ -173,7 +180,7 @@ export default class DexSettingsPage extends BasePage { }, 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,35 @@ 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, disable: true } + 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/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 + window.location.assign(`/dexsettings/${host}`) } async prepareAccountDisable (disableAccountForm: HTMLElement) { @@ -402,7 +423,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/locales.ts b/client/webserver/site/src/js/locales.ts index b8c2520bfc..471f193dd9 100644 --- a/client/webserver/site/src/js/locales.ts +++ b/client/webserver/site/src/js/locales.ts @@ -195,6 +195,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 a5ac72cd61..487e26b1fb 100644 --- a/client/webserver/site/src/js/markets.ts +++ b/client/webserver/site/src/js/markets.ts @@ -1092,7 +1092,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 1974d98c6e..99231d529e 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 {