diff --git a/client/comms/wsconn.go b/client/comms/wsconn.go index 04d8071b65..0992a9f25d 100644 --- a/client/comms/wsconn.go +++ b/client/comms/wsconn.go @@ -41,6 +41,15 @@ const ( DefaultResponseTimeout = 30 * time.Second ) +// ConnectionStatus represents the current status of the websocket connection. +type ConnectionStatus uint32 + +const ( + Disconnected ConnectionStatus = iota + Connected + InvalidCert +) + // ErrInvalidCert is the error returned when attempting to use an invalid cert // to set up a ws connection. var ErrInvalidCert = fmt.Errorf("invalid certificate") @@ -88,7 +97,7 @@ type WsCfg struct { // // NOTE: Disconnect event notifications may lag behind actual // disconnections. - ConnectEventFunc func(bool) + ConnectEventFunc func(ConnectionStatus) // Logger is the logger for the WsConn. Logger dex.Logger @@ -112,8 +121,8 @@ type wsConn struct { wsMtx sync.Mutex ws *websocket.Conn - connectedMtx sync.RWMutex - connected bool + connectedMtx sync.RWMutex + connectionStatus ConnectionStatus reqMtx sync.RWMutex respHandlers map[uint64]*responseHandler @@ -165,18 +174,18 @@ func NewWsConn(cfg *WsCfg) (WsConn, error) { func (conn *wsConn) IsDown() bool { conn.connectedMtx.RLock() defer conn.connectedMtx.RUnlock() - return !conn.connected + return conn.connectionStatus != Connected } -// setConnected updates the connection's connected state and runs the +// setConnectionStatus updates the connection's status and runs the // ConnectEventFunc in case of a change. -func (conn *wsConn) setConnected(connected bool) { +func (conn *wsConn) setConnectionStatus(status ConnectionStatus) { conn.connectedMtx.Lock() - statusChange := conn.connected != connected - conn.connected = connected + statusChange := conn.connectionStatus != status + conn.connectionStatus = status conn.connectedMtx.Unlock() if statusChange && conn.cfg.ConnectEventFunc != nil { - conn.cfg.ConnectEventFunc(connected) + conn.cfg.ConnectEventFunc(status) } } @@ -195,11 +204,13 @@ func (conn *wsConn) connect(ctx context.Context) error { if err != nil { var e x509.UnknownAuthorityError if errors.As(err, &e) { + conn.setConnectionStatus(InvalidCert) if conn.tlsCfg == nil { return ErrCertRequired } return ErrInvalidCert } + conn.setConnectionStatus(Disconnected) return err } @@ -241,7 +252,7 @@ func (conn *wsConn) connect(ctx context.Context) error { conn.ws = ws conn.wsMtx.Unlock() - conn.setConnected(true) + conn.setConnectionStatus(Connected) conn.wg.Add(1) go func() { defer conn.wg.Done() @@ -264,7 +275,7 @@ func (conn *wsConn) close() { // run as a goroutine. Increment the wg before calling read. func (conn *wsConn) read(ctx context.Context) { reconnect := func() { - conn.setConnected(false) + conn.setConnectionStatus(Disconnected) conn.reconnectCh <- struct{}{} } @@ -416,7 +427,7 @@ func (conn *wsConn) Connect(ctx context.Context) (*sync.WaitGroup, error) { go func() { defer conn.wg.Done() <-ctxInternal.Done() - conn.setConnected(false) + conn.setConnectionStatus(Disconnected) conn.wsMtx.Lock() if conn.ws != nil { conn.log.Debug("Sending close 1000 (normal) message.") diff --git a/client/core/core.go b/client/core/core.go index 21fcb6cb67..25b03e0b7d 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -134,8 +134,9 @@ type dexConnection struct { epochMtx sync.RWMutex epoch map[string]uint64 - // connected is a best guess on the ws connection status. - connected uint32 + + // connectionStatus is a best guess on the ws connection status. + connectionStatus uint32 pendingFeeMtx sync.RWMutex pendingFee *pendingFeeState @@ -294,12 +295,14 @@ func (dc *dexConnection) exchangeInfo() *Exchange { dc.cfgMtx.RLock() cfg := dc.cfg dc.cfgMtx.RUnlock() + connectionStatus := comms.ConnectionStatus( + atomic.LoadUint32(&dc.connectionStatus)) if cfg == nil { // no config, assets, or markets data return &Exchange{ - Host: dc.acct.host, - AcctID: acctID, - Connected: atomic.LoadUint32(&dc.connected) == 1, - PendingFee: dc.getPendingFee(), + Host: dc.acct.host, + AcctID: acctID, + ConnectionStatus: connectionStatus, + PendingFee: dc.getPendingFee(), } } @@ -328,16 +331,18 @@ func (dc *dexConnection) exchangeInfo() *Exchange { feeAssets["dcr"] = dcrAsset } + connectionStatus = comms.ConnectionStatus( + atomic.LoadUint32(&dc.connectionStatus)) return &Exchange{ - Host: dc.acct.host, - AcctID: acctID, - Markets: dc.marketMap(), - Assets: assets, - Connected: atomic.LoadUint32(&dc.connected) == 1, - Fee: dcrAsset, - RegFees: feeAssets, - PendingFee: dc.getPendingFee(), - CandleDurs: cfg.BinSizes, + Host: dc.acct.host, + AcctID: acctID, + Markets: dc.marketMap(), + Assets: assets, + ConnectionStatus: connectionStatus, + Fee: dcrAsset, + RegFees: feeAssets, + PendingFee: dc.getPendingFee(), + CandleDurs: cfg.BinSizes, } } @@ -947,10 +952,12 @@ func (c *Core) dex(addr string) (*dexConnection, bool, error) { c.connMtx.RLock() dc, found := c.conns[host] c.connMtx.RUnlock() - connected := found && atomic.LoadUint32(&dc.connected) == 1 if !found { return nil, false, fmt.Errorf("unknown DEX %s", addr) } + connectionStatus := comms.ConnectionStatus( + atomic.LoadUint32(&dc.connectionStatus)) + connected := connectionStatus == comms.Connected return dc, connected, nil } @@ -2769,6 +2776,31 @@ func (c *Core) EstimateRegistrationTxFee(host string, certI interface{}, assetID return txFee, nil } +// UpdateCert attempts to connect to a server using a new TLS certificate. If +// the connection is successful, then the cert in the database is updated. +func (c *Core) UpdateCert(host string, cert []byte) error { + account, err := c.db.Account(host) + if err != nil { + return err + } + if account == nil { + return fmt.Errorf("account does not exist for host: %v", host) + } + account.Cert = cert + + _, connected := c.connectAccount(account) + if !connected { + return errors.New("failed to connect using new cert") + } + + err = c.db.UpdateAccountInfo(account) + if err != nil { + return fmt.Errorf("failed to update account info: %w", err) + } + + return nil +} + // Register registers an account with a new DEX. If an error occurs while // fetching the DEX configuration or creating the fee transaction, it will be // returned immediately. @@ -5555,6 +5587,7 @@ func (c *Core) connectDEX(acctInfo *db.AccountInfo, temporary ...bool) (*dexConn apiVer: -1, reportingConnects: reporting, spots: make(map[string]*msgjson.Spot), + connectionStatus: uint32(comms.Disconnected), // On connect, must set: cfg, epoch, and assets. } @@ -5584,8 +5617,8 @@ func (c *Core) connectDEX(acctInfo *db.AccountInfo, temporary ...bool) (*dexConn return nil, errors.New("a TLS connection is required when not using a hidden service") } - wsCfg.ConnectEventFunc = func(connected bool) { - c.handleConnectEvent(dc, connected) + wsCfg.ConnectEventFunc = func(status comms.ConnectionStatus) { + c.handleConnectEvent(dc, status) } wsCfg.ReconnectSync = func() { go c.handleReconnect(host) @@ -5739,11 +5772,9 @@ func (dc *dexConnection) broadcastingConnect() bool { // lost or established. // // NOTE: Disconnect event notifications may lag behind actual disconnections. -func (c *Core) handleConnectEvent(dc *dexConnection, connected bool) { - var v uint32 +func (c *Core) handleConnectEvent(dc *dexConnection, status comms.ConnectionStatus) { topic := TopicDEXDisconnected - if connected { - v = 1 + if status == comms.Connected { topic = TopicDEXConnected } else { for _, tracker := range dc.trackedTrades() { @@ -5759,10 +5790,10 @@ func (c *Core) handleConnectEvent(dc *dexConnection, connected bool) { tracker.mtx.Unlock() } } - atomic.StoreUint32(&dc.connected, v) + atomic.StoreUint32(&dc.connectionStatus, uint32(status)) if dc.broadcastingConnect() { subject, details := c.formatDetails(topic, dc.acct.host) - dc.notify(newConnEventNote(topic, subject, dc.acct.host, connected, details, db.Poke)) + dc.notify(newConnEventNote(topic, subject, dc.acct.host, status, details, db.Poke)) } } diff --git a/client/core/core_test.go b/client/core/core_test.go index 7db7614715..7a34ac044b 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -267,7 +267,7 @@ func testDexConnection(ctx context.Context, crypter *tCrypter) (*dexConnection, trades: make(map[order.OrderID]*trackedTrade), epoch: map[string]uint64{tDcrBtcMktName: 0}, apiVer: serverdex.PreAPIVersion, - connected: 1, + connectionStatus: uint32(comms.Connected), reportingConnects: 1, spots: make(map[string]*msgjson.Spot), }, conn, acct @@ -396,7 +396,7 @@ func (tdb *TDB) DisableAccount(url string) error { return tdb.disableAccountErr } -func (tdb *TDB) UpdateAccount(ai *db.AccountInfo) error { +func (tdb *TDB) UpdateAccountInfo(ai *db.AccountInfo) error { tdb.verifyUpdateAccount = true tdb.acct = ai return nil @@ -2662,12 +2662,12 @@ wait: rig.dc.acct.unlock(rig.crypter) // DEX not connected - atomic.StoreUint32(&rig.dc.connected, 0) + atomic.StoreUint32(&rig.dc.connectionStatus, uint32(comms.Disconnected)) _, err = tCore.Trade(tPW, form) if err == nil { t.Fatalf("no error for disconnected dex") } - atomic.StoreUint32(&rig.dc.connected, 1) + atomic.StoreUint32(&rig.dc.connectionStatus, uint32(comms.Connected)) // No base asset form.Base = 12345 diff --git a/client/core/notification.go b/client/core/notification.go index 9b6be2e0f2..62936fd9e4 100644 --- a/client/core/notification.go +++ b/client/core/notification.go @@ -6,6 +6,7 @@ package core import ( "fmt" + "decred.org/dcrdex/client/comms" "decred.org/dcrdex/client/db" "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/msgjson" @@ -335,8 +336,8 @@ func (on *EpochNotification) String() string { // ConnEventNote is a notification regarding individual DEX connection status. type ConnEventNote struct { db.Notification - Host string `json:"host"` - Connected bool `json:"connected"` + Host string `json:"host"` + ConnectionStatus comms.ConnectionStatus `json:"connectionStatus"` } const ( @@ -344,11 +345,11 @@ const ( TopicDEXDisconnected Topic = "DEXDisconnected" ) -func newConnEventNote(topic Topic, subject, host string, connected bool, details string, severity db.Severity) *ConnEventNote { +func newConnEventNote(topic Topic, subject, host string, status comms.ConnectionStatus, details string, severity db.Severity) *ConnEventNote { return &ConnEventNote{ - Notification: db.NewNotification(NoteTypeConnEvent, topic, subject, details, severity), - Host: host, - Connected: connected, + Notification: db.NewNotification(NoteTypeConnEvent, topic, subject, details, severity), + Host: host, + ConnectionStatus: status, } } diff --git a/client/core/trade_simnet_test.go b/client/core/trade_simnet_test.go index b113e22fed..b132e59206 100644 --- a/client/core/trade_simnet_test.go +++ b/client/core/trade_simnet_test.go @@ -39,6 +39,7 @@ import ( "decred.org/dcrdex/client/asset/btc" "decred.org/dcrdex/client/asset/dcr" + "decred.org/dcrdex/client/comms" "decred.org/dcrdex/client/db" "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/calc" @@ -677,7 +678,7 @@ func TestOrderStatusReconciliation(t *testing.T) { disconnectTimeout := 10 * sleepFactor * time.Second disconnected := client2.notes.find(context.Background(), disconnectTimeout, func(n Notification) bool { connNote, ok := n.(*ConnEventNote) - return ok && connNote.Host == dexHost && !connNote.Connected + return ok && connNote.Host == dexHost && connNote.ConnectionStatus != comms.Connected }) if !disconnected { t.Fatalf("client 2 dex not disconnected after %v", disconnectTimeout) diff --git a/client/core/types.go b/client/core/types.go index 80236c5d56..5cef60f205 100644 --- a/client/core/types.go +++ b/client/core/types.go @@ -11,6 +11,7 @@ import ( "sync" "decred.org/dcrdex/client/asset" + "decred.org/dcrdex/client/comms" "decred.org/dcrdex/client/db" "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/calc" @@ -491,15 +492,15 @@ type PendingFeeState struct { // Exchange represents a single DEX with any number of markets. type Exchange struct { - Host string `json:"host"` - AcctID string `json:"acctID"` - Markets map[string]*Market `json:"markets"` - Assets map[uint32]*dex.Asset `json:"assets"` - Connected bool `json:"connected"` - Fee *FeeAsset `json:"feeAsset"` // DEPRECATED. DCR. - RegFees map[string]*FeeAsset `json:"regFees"` - PendingFee *PendingFeeState `json:"pendingFee,omitempty"` - CandleDurs []string `json:"candleDurs"` + Host string `json:"host"` + AcctID string `json:"acctID"` + Markets map[string]*Market `json:"markets"` + Assets map[uint32]*dex.Asset `json:"assets"` + ConnectionStatus comms.ConnectionStatus `json:"connectionStatus"` + Fee *FeeAsset `json:"feeAsset"` // DEPRECATED. DCR. + RegFees map[string]*FeeAsset `json:"regFees"` + PendingFee *PendingFeeState `json:"pendingFee,omitempty"` + CandleDurs []string `json:"candleDurs"` } // newDisplayID creates a display-friendly market ID for a base/quote ID pair. diff --git a/client/db/bolt/db.go b/client/db/bolt/db.go index acf6e67e8a..4cc805b7fa 100644 --- a/client/db/bolt/db.go +++ b/client/db/bolt/db.go @@ -521,6 +521,19 @@ func (db *BoltDB) CreateAccount(ai *dexdb.AccountInfo) error { }) } +// UpdateAccountInfo updates the account info for an existing account with +// the same Host as the parameter. If no account exists with this host, +// an error is returned. +func (db *BoltDB) UpdateAccountInfo(ai *dexdb.AccountInfo) error { + return db.acctsUpdate(func(accts *bbolt.Bucket) error { + acct := accts.Bucket([]byte(ai.Host)) + if acct == nil { + return fmt.Errorf("account not found for %s", ai.Host) + } + return acct.Put(accountKey, ai.Encode()) + }) +} + // deleteAccount removes the account by host. func (db *BoltDB) deleteAccount(host string) error { acctKey := []byte(host) diff --git a/client/db/interface.go b/client/db/interface.go index 05de76c931..c02dc989fb 100644 --- a/client/db/interface.go +++ b/client/db/interface.go @@ -34,6 +34,10 @@ type DB interface { Account(host string) (*AccountInfo, error) // CreateAccount saves the AccountInfo. CreateAccount(ai *AccountInfo) error + // UpdateAccountInfo updates the account info for an existing account with + // the same Host as the parameter. If no account exists with this host, + // an error is returned. + UpdateAccountInfo(ai *AccountInfo) error // DisableAccount sets the AccountInfo disabled status to true. DisableAccount(host string) error // AccountProof retrieves the AccountPoof value specified by url. diff --git a/client/webserver/api.go b/client/webserver/api.go index 30f39a226f..e2e22f7787 100644 --- a/client/webserver/api.go +++ b/client/webserver/api.go @@ -375,6 +375,24 @@ func (s *WebServer) apiAccountImport(w http.ResponseWriter, r *http.Request) { writeJSON(w, simpleAck(), s.indent) } +func (s *WebServer) apiUpdateCert(w http.ResponseWriter, r *http.Request) { + form := &struct { + Host string `json:"host"` + Cert string `json:"cert"` + }{} + if !readPost(w, r, form) { + return + } + + err := s.core.UpdateCert(form.Host, []byte(form.Cert)) + if err != nil { + s.writeAPIError(w, fmt.Errorf("error updating cert: %w", err)) + return + } + + writeJSON(w, simpleAck(), s.indent) +} + // apiAccountDisable is the handler for the '/disableaccount' API request. func (s *WebServer) apiAccountDisable(w http.ResponseWriter, r *http.Request) { form := new(accountDisableForm) diff --git a/client/webserver/http.go b/client/webserver/http.go index 9d1b914038..6cbc3bd18a 100644 --- a/client/webserver/http.go +++ b/client/webserver/http.go @@ -244,6 +244,34 @@ func (s *WebServer) handleSettings(w http.ResponseWriter, r *http.Request) { s.sendTemplate(w, "settings", data) } +// handleDexSettings is the handler for the '/dexsettings' page request. +func (s *WebServer) handleDexSettings(w http.ResponseWriter, r *http.Request) { + host, err := getHostCtx(r) + if err != nil { + log.Errorf("error getting host ctx: %v", err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + exchanges := s.core.Exchanges() + exchange, ok := exchanges[host] + if !ok { + log.Errorf("no exchange with host: %v", host) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + data := &struct { + CommonArguments + Exchange *core.Exchange + }{ + CommonArguments: *commonArgs(r, fmt.Sprintf("%v Settings | Decred DEX", host)), + Exchange: exchange, + } + + s.sendTemplate(w, "dexsettings", data) +} + type ordersTmplData struct { CommonArguments Assets map[uint32]*core.SupportedAsset diff --git a/client/webserver/live_test.go b/client/webserver/live_test.go index b55ca7469a..b5937825ae 100644 --- a/client/webserver/live_test.go +++ b/client/webserver/live_test.go @@ -25,6 +25,7 @@ import ( "decred.org/dcrdex/client/asset/btc" "decred.org/dcrdex/client/asset/dcr" "decred.org/dcrdex/client/asset/ltc" + "decred.org/dcrdex/client/comms" "decred.org/dcrdex/client/core" "decred.org/dcrdex/client/db" "decred.org/dcrdex/dex" @@ -303,7 +304,7 @@ var tExchanges = map[string]*core.Exchange{ mkid(22, 42): mkMrkt("mona", "dcr"), mkid(28, 0): mkMrkt("vtc", "btc"), }, - Connected: true, + ConnectionStatus: comms.Connected, RegFees: map[string]*core.FeeAsset{ "dcr": { ID: 42, @@ -352,7 +353,7 @@ var tExchanges = map[string]*core.Exchange{ mkid(0, 2): mkMrkt("btc", "ltc"), mkid(22, 141): mkMrkt("mona", "kmd"), }, - Connected: true, + ConnectionStatus: comms.Connected, RegFees: map[string]*core.FeeAsset{ "dcr": { ID: 42, @@ -1586,6 +1587,9 @@ func (c *TCore) ExportSeed(pw []byte) ([]byte, error) { func (c *TCore) WalletLogFilePath(uint32) (string, error) { return "", nil } +func (c *TCore) UpdateCert(string, []byte) error { + return nil +} func TestServer(t *testing.T) { // Register dummy drivers for unimplemented assets. diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index f52e16ad99..0cc301531b 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -215,4 +215,5 @@ var EnUS = map[string]string{ "Maximum Possible Swap Fees": "Maximum Possible Swap Fees", "max_fee_conditions": "This is the most you would ever pay in fees on your swap. Fees are normally assessed at a fraction of this rate. The maximum is not subject to changes once your order is placed.", "wallet_logs": "Wallet Logs", + "Update TLS Certificate": "Update TLS Certificate", } diff --git a/client/webserver/middleware.go b/client/webserver/middleware.go index f3e39fa494..43065ebbfe 100644 --- a/client/webserver/middleware.go +++ b/client/webserver/middleware.go @@ -19,6 +19,7 @@ type ctxID int const ( ctxOID ctxID = iota + ctxHost ) // securityMiddleware adds security headers to the server responses. @@ -133,7 +134,29 @@ func (s *WebServer) requireDEXConnection(next http.Handler) http.Handler { }) } -// orderIDCtx embeds order ID into the request context +// dexHostCtx embeds the host into the request context. +func dexHostCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + host := chi.URLParam(r, "host") + ctx := context.WithValue(r.Context(), ctxHost, host) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// getHostCtx interprets the context value at ctxHost as a string host. +func getHostCtx(r *http.Request) (string, error) { + untypedHost := r.Context().Value(ctxHost) + if untypedHost == nil { + return "", errors.New("host not set in request") + } + host, ok := untypedHost.(string) + if !ok { + return "", errors.New("type assertion failed") + } + return host, nil +} + +// orderIDCtx embeds order ID into the request context. func orderIDCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { oid := chi.URLParam(r, "oid") diff --git a/client/webserver/site/src/css/application.scss b/client/webserver/site/src/css/application.scss index 0f2265371e..48f3cabf6a 100644 --- a/client/webserver/site/src/css/application.scss +++ b/client/webserver/site/src/css/application.scss @@ -78,3 +78,4 @@ $grid-breakpoints: ( @import "./settings.scss"; @import "./forms.scss"; @import "./forms_dark.scss"; +@import "./dex_settings.scss"; diff --git a/client/webserver/site/src/css/dex_settings.scss b/client/webserver/site/src/css/dex_settings.scss new file mode 100644 index 0000000000..b0600bb70f --- /dev/null +++ b/client/webserver/site/src/css/dex_settings.scss @@ -0,0 +1,7 @@ +.connection { + color: green; +} + +.disconnected { + color: red; +} diff --git a/client/webserver/site/src/css/icons.scss b/client/webserver/site/src/css/icons.scss index 46e76949bd..15c3787785 100644 --- a/client/webserver/site/src/css/icons.scss +++ b/client/webserver/site/src/css/icons.scss @@ -111,3 +111,7 @@ .ico-checkbox::before { content: "\e90d"; } + +.ico-connection::before { + content: "\e91b"; +} diff --git a/client/webserver/site/src/css/settings.scss b/client/webserver/site/src/css/settings.scss index ea962ae73f..0ed3a2df3a 100644 --- a/client/webserver/site/src/css/settings.scss +++ b/client/webserver/site/src/css/settings.scss @@ -49,3 +49,8 @@ div.settings-exchange { word-break: break-all; user-select: all; } + +.dex-settings-icon { + font-size: 15; + margin-left: 0.5rem; +} diff --git a/client/webserver/site/src/font/icomoon.svg b/client/webserver/site/src/font/icomoon.svg index 1b4f26b54c..c2186cb555 100644 --- a/client/webserver/site/src/font/icomoon.svg +++ b/client/webserver/site/src/font/icomoon.svg @@ -7,25 +7,26 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/webserver/site/src/font/icomoon.ttf b/client/webserver/site/src/font/icomoon.ttf index fe02edc344..65b5dafd85 100644 Binary files a/client/webserver/site/src/font/icomoon.ttf and b/client/webserver/site/src/font/icomoon.ttf differ diff --git a/client/webserver/site/src/font/icomoon.woff b/client/webserver/site/src/font/icomoon.woff index 35102c1297..9da6dd9054 100644 Binary files a/client/webserver/site/src/font/icomoon.woff and b/client/webserver/site/src/font/icomoon.woff differ diff --git a/client/webserver/site/src/html/dexsettings.tmpl b/client/webserver/site/src/html/dexsettings.tmpl new file mode 100644 index 0000000000..45fbe9f73d --- /dev/null +++ b/client/webserver/site/src/html/dexsettings.tmpl @@ -0,0 +1,40 @@ +{{define "dexsettings"}} +{{template "top" .}} +{{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+ +

{{.Exchange.Host}}

+
+ + +

+
+
+
+
+ +
+
+ +
+
+ + +
+
+ +
+ {{- /* DISABLE ACCOUNT */ -}} +
+ {{template "disableAccountForm"}} +
+ + {{- /* AUTHORIZE EXPORT ACCOUNT */ -}} +
+ {{template "authorizeAccountExportForm"}} +
+
+ +
+{{template "bottom"}} +{{end}} diff --git a/client/webserver/site/src/html/settings.tmpl b/client/webserver/site/src/html/settings.tmpl index 83cbf8ce36..4c7d113a56 100644 --- a/client/webserver/site/src/html/settings.tmpl +++ b/client/webserver/site/src/html/settings.tmpl @@ -2,9 +2,7 @@ {{template "top" .}} {{$passwordIsCached := .UserInfo.PasswordIsCached}}
-
-
@@ -19,27 +17,14 @@
+
Registered Dexes:
{{range $host, $xc := .UserInfo.Exchanges}} -
- - [[[DEX Address]]]: {{$host}} - -
- [[[Account ID]]]: - {{if eq (len $xc.AcctID) 0}} - <login to show> - {{else}} - {{$xc.AcctID}} -
- {{end}}
- - -
+ {{end}}

- +

[[[simultaneous_servers_msg]]]

@@ -74,11 +59,6 @@ {{template "confirmRegistrationForm"}} - {{- /* AUTHORIZE EXPORT ACCOUNT */ -}} -
- {{template "authorizeAccountExportForm"}} -
- {{- /* AUTHORIZE IMPORT ACCOUNT */ -}}
{{template "authorizeAccountImportForm" .}} @@ -89,11 +69,6 @@ {{template "newWalletForm" }}
- {{- /* DISABLE ACCOUNT */ -}} -
- {{template "disableAccountForm"}} -
- {{- /* CHANGE APP PASSWORD */ -}}
{{template "changeAppPWForm"}} diff --git a/client/webserver/site/src/js/app.ts b/client/webserver/site/src/js/app.ts index d5fcc92b1c..f51b25c9f0 100644 --- a/client/webserver/site/src/js/app.ts +++ b/client/webserver/site/src/js/app.ts @@ -7,6 +7,7 @@ import SettingsPage from './settings' import MarketsPage from './markets' import OrdersPage from './orders' import OrderPage from './order' +import DexSettingsPage from './dexsettings' import { RateEncodingFactor, StatusExecuted, hasLiveMatches } from './orderutil' import { getJSON, postJSON } from './http' import * as ntfn from './notifications' @@ -67,7 +68,8 @@ const constructors: Record = { wallets: WalletsPage, settings: SettingsPage, orders: OrdersPage, - order: OrderPage + order: OrderPage, + dexsettings: DexSettingsPage } // unathedPages are pages that don't require authorization to load. @@ -231,6 +233,7 @@ export default class Application { /* Load the page from the server. Insert and bind the DOM. */ async loadPage (page: string, data?: any, skipPush?: boolean): Promise { + console.log('in load page') // Close some menus and tooltips. this.tooltip.style.left = '-10000px' Doc.hide(this.page.noteBox, this.page.profileBox) @@ -247,6 +250,7 @@ export default class Application { // Append the request to the page history. if (!skipPush) { const path = delivered === requestedHandler ? url.toString() : `/${delivered}` + console.log(`data - ${JSON.stringify(data)}`) window.history.pushState({ page: page, data: data }, '', path) } // Insert page and attach handlers. @@ -419,6 +423,7 @@ export default class Application { } Doc.bind(a, 'click', (e: Event) => { e.preventDefault() + console.log('overridden click') this.loadPage(token, params) }) } @@ -578,7 +583,9 @@ export default class Application { case 'conn': { const n = note as ConnEventNote const xc = this.user.exchanges[n.host] - if (xc) xc.connected = n.connected + if (xc) { + xc.connectionStatus = n.connectionStatus + } break } case 'spots': { @@ -730,6 +737,7 @@ export default class Application { */ haveAssetOrders (assetID: number): boolean { for (const xc of Object.values(this.user.exchanges)) { + if (!xc.markets) continue for (const market of Object.values(xc.markets)) { if (!market.orders) continue for (const ord of market.orders) { diff --git a/client/webserver/site/src/js/dexsettings.ts b/client/webserver/site/src/js/dexsettings.ts new file mode 100644 index 0000000000..4ca3867227 --- /dev/null +++ b/client/webserver/site/src/js/dexsettings.ts @@ -0,0 +1,192 @@ +import Doc from './doc' +import BasePage from './basepage' +import State from './state' +import { postJSON } from './http' +import * as forms from './forms' + +import { + app, + PageElement, + ConnectionStatus +} from './registry' + +const animationLength = 300 + +export default class DexSettingsPage extends BasePage { + body: HTMLElement + forms: PageElement[] + currentForm: PageElement + page: Record + host: string + keyup: (e: KeyboardEvent) => void + + constructor (body: HTMLElement) { + super() + this.body = body + this.host = body.dataset.host ? body.dataset.host : '' + const page = this.page = Doc.idDescendants(body) + this.forms = Doc.applySelector(page.forms, ':scope > form') + + Doc.bind(page.exportDexBtn, 'click', () => this.prepareAccountExport(page.authorizeAccountExportForm)) + Doc.bind(page.disableAcctBtn, 'click', () => this.prepareAccountDisable(page.disableAccountForm)) + Doc.bind(page.updateCertBtn, 'click', () => page.certFileInput.click()) + Doc.bind(page.certFileInput, 'change', () => this.onCertFileChange()) + + forms.bind(page.authorizeAccountExportForm, page.authorizeExportAccountConfirm, () => this.exportAccount()) + forms.bind(page.disableAccountForm, page.disableAccountConfirm, () => this.disableAccount()) + + const closePopups = () => { + Doc.hide(page.forms) + page.exportSeedPW.value = '' + page.seedDiv.textContent = '' + } + + Doc.bind(page.forms, 'mousedown', (e: MouseEvent) => { + if (!Doc.mouseInElement(e, this.currentForm)) { closePopups() } + }) + + this.keyup = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + closePopups() + } + } + Doc.bind(document, 'keyup', this.keyup) + + page.forms.querySelectorAll('.form-closer').forEach(el => { + Doc.bind(el, 'click', () => { closePopups() }) + }) + + this.notifiers = { + conn: () => { this.setConnectionStatus() } + } + + this.setConnectionStatus() + } + + /* showForm shows a modal form with a little animation. */ + async showForm (form: HTMLElement) { + const page = this.page + this.currentForm = form + this.forms.forEach(form => Doc.hide(form)) + form.style.right = '10000px' + Doc.show(page.forms, form) + const shift = (page.forms.offsetWidth + form.offsetWidth) / 2 + await Doc.animate(animationLength, progress => { + form.style.right = `${(1 - progress) * shift}px` + }, 'easeOutHard') + form.style.right = '0' + } + + // exportAccount exports and downloads the account info. + async exportAccount () { + const page = this.page + const pw = page.exportAccountAppPass.value + const host = page.exportAccountHost.textContent + page.exportAccountAppPass.value = '' + const req = { + pw, + host + } + const loaded = app().loading(this.body) + const res = await postJSON('/api/exportaccount', req) + loaded() + if (!app().checkResponse(res)) { + page.exportAccountErr.textContent = res.msg + Doc.show(page.exportAccountErr) + return + } + const accountForExport = JSON.parse(JSON.stringify(res.account)) + const a = document.createElement('a') + a.setAttribute('download', 'dcrAccount-' + host + '.json') + a.setAttribute('href', 'data:text/json,' + JSON.stringify(accountForExport, null, 2)) + a.click() + Doc.hide(page.forms) + } + + // disableAccount disables the account associated with the provided host. + async disableAccount () { + const page = this.page + const pw = page.disableAccountAppPW.value + const host = page.disableAccountHost.textContent + page.disableAccountAppPW.value = '' + const req = { + pw, + host + } + const loaded = app().loading(this.body) + const res = await postJSON('/api/disableaccount', req) + loaded() + if (!app().checkResponse(res, true)) { + page.disableAccountErr.textContent = res.msg + Doc.show(page.disableAccountErr) + return + } + Doc.hide(page.forms) + window.location.assign('/settings') + } + + async prepareAccountExport (authorizeAccountExportForm: HTMLElement) { + const page = this.page + page.exportAccountHost.textContent = this.host + page.exportAccountErr.textContent = '' + if (State.passwordIsCached()) { + this.exportAccount() + } else { + this.showForm(authorizeAccountExportForm) + } + } + + async prepareAccountDisable (disableAccountForm: HTMLElement) { + const page = this.page + page.disableAccountHost.textContent = this.host + page.disableAccountErr.textContent = '' + this.showForm(disableAccountForm) + } + + async onCertFileChange () { + const page = this.page + Doc.hide(page.errMsg) + const files = page.certFileInput.files + let cert + if (files && files.length) cert = await files[0].text() + console.log(cert) + if (!cert) return + const req = { host: this.host, cert: cert } + const loaded = app().loading(this.body) + const res = await postJSON('/api/updatecert', req) + loaded() + if (!app().checkResponse(res, true)) { + page.errMsg.textContent = res.msg + Doc.show(page.errMsg) + } + } + + setConnectionStatus () { + const page = this.page + const exchange = app().user.exchanges[this.host] + const displayIcons = (connected: boolean) => { + if (connected) { + Doc.hide(page.disconnectedIcon) + Doc.show(page.connectedIcon) + } else { + Doc.show(page.disconnectedIcon) + Doc.hide(page.connectedIcon) + } + } + if (exchange) { + switch (exchange.connectionStatus) { + case ConnectionStatus.Connected: + displayIcons(true) + page.connectionStatus.textContent = 'Connected' + break + case ConnectionStatus.Disconnected: + displayIcons(false) + page.connectionStatus.textContent = 'Disconnected' + break + case ConnectionStatus.InvalidCert: + displayIcons(false) + page.connectionStatus.textContent = 'Disconnected - Invalid Certificate' + } + } + } +} diff --git a/client/webserver/site/src/js/markets.ts b/client/webserver/site/src/js/markets.ts index 12d6c7851e..90cd687143 100644 --- a/client/webserver/site/src/js/markets.ts +++ b/client/webserver/site/src/js/markets.ts @@ -48,7 +48,8 @@ import { RemainderUpdate, ConnEventNote, Spot, - OrderOption + OrderOption, + ConnectionStatus } from './registry' const bind = Doc.bind @@ -239,6 +240,7 @@ export default class MarketsPage extends BasePage { // Prepare the list of markets. this.marketList = new MarketList(page.marketList) for (const xc of this.marketList.xcSections) { + if (!xc.marketRows) continue for (const row of xc.marketRows) { bind(row.node, 'click', () => { this.setMarket(xc.host, row.mkt.baseid, row.mkt.quoteid) @@ -614,7 +616,7 @@ export default class MarketsPage extends BasePage { // If dex is not connected to server, is not possible to know fee // registration status. - if (!dex.connected) return + if (dex.connectionStatus !== ConnectionStatus.Connected) return this.updateRegistrationStatusView() @@ -665,7 +667,7 @@ export default class MarketsPage extends BasePage { // If we have not yet connected, there is no dex.assets or any other // 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.connected) { + if (dex.connectionStatus !== ConnectionStatus.Connected) { // TODO: Figure out why this was like this. // this.market = { dex: dex } @@ -1714,7 +1716,7 @@ export default class MarketsPage extends BasePage { } setBalanceVisibility () { - if (this.market.dex.connected) { + if (this.market.dex.connectionStatus === ConnectionStatus.Connected) { Doc.show(this.page.balanceTable) } else { Doc.hide(this.page.balanceTable) @@ -1726,7 +1728,7 @@ export default class MarketsPage extends BasePage { this.setBalanceVisibility() // if connection to dex server fails, it is not possible to retrieve // markets. - if (!this.market.dex.connected) return + if (this.market.dex.connectionStatus !== ConnectionStatus.Connected) return this.balanceWgt.updateAsset(note.assetID) // If there's a balance update, refresh the max order section. const mkt = this.market @@ -2047,7 +2049,7 @@ export default class MarketsPage extends BasePage { */ async handleConnNote (note: ConnEventNote) { this.marketList.setConnectionStatus(note) - if (note.connected) { + if (note.connectionStatus === ConnectionStatus.Connected) { // Having been disconnected from a DEX server, anything may have changed, // or this may be the first opportunity to get the server's config, so // fetch it all before reloading the markets page. @@ -2236,7 +2238,7 @@ class MarketList { setConnectionStatus (note: ConnEventNote) { const xcSection = this.xcSection(note.host) if (!xcSection) return console.error(`setConnectionStatus: no exchange section for ${note.host}`) - xcSection.setConnected(note.connected) + xcSection.setConnected(note.connectionStatus === ConnectionStatus.Connected) } /* @@ -2267,7 +2269,7 @@ class ExchangeSection { tmpl.header.textContent = dex.host this.disconnectedIco = tmpl.disconnected - if (dex.connected) Doc.hide(tmpl.disconnected) + if (dex.connectionStatus === ConnectionStatus.Connected) Doc.hide(tmpl.disconnected) tmpl.mkts.removeChild(tmpl.mktrow) // If disconnected is not possible to get the markets from the server. @@ -2288,6 +2290,7 @@ class ExchangeSection { * symbol first, quote symbol second. */ sortedMarkets () { + if (!this.marketRows) return [] return [...this.marketRows].sort((a, b) => a.name < b.name ? -1 : 1) } diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts index 8fd005d05c..17398cd934 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -8,12 +8,18 @@ declare global { } } +export enum ConnectionStatus { + Disconnected = 0, + Connected = 1, + InvalidCert = 2, +} + export interface Exchange { host: string acctID: string markets: Record assets: Record - connected: boolean + connectionStatus: ConnectionStatus feeAsset: FeeAsset // DEPRECATED. DCR. regFees: Record pendingFee: PendingFeeState | null @@ -275,7 +281,7 @@ export interface MatchNote extends CoreNote { export interface ConnEventNote extends CoreNote { host: string - connected: boolean + connectionStatus: ConnectionStatus } export interface OrderNote extends CoreNote { diff --git a/client/webserver/site/src/js/settings.ts b/client/webserver/site/src/js/settings.ts index 205101357e..211150eb83 100644 --- a/client/webserver/site/src/js/settings.ts +++ b/client/webserver/site/src/js/settings.ts @@ -111,21 +111,6 @@ export default class SettingsPage extends BasePage { this.animateRegAsset(page.dexAddrForm) }) - forms.bind(page.authorizeAccountExportForm, page.authorizeExportAccountConfirm, () => this.exportAccount()) - forms.bind(page.disableAccountForm, page.disableAccountConfirm, () => this.disableAccount()) - - const exchangesDiv = page.exchanges - if (typeof app().user.exchanges !== 'undefined') { - for (const host of Object.keys(app().user.exchanges)) { - // Bind export account button click event. - const exportAccountButton = Doc.tmplElement(exchangesDiv, `exportAccount-${host}`) - Doc.bind(exportAccountButton, 'click', () => this.prepareAccountExport(host, page.authorizeAccountExportForm)) - // Bind disable account button click event. - const disableAccountButton = Doc.tmplElement(exchangesDiv, `disableAccount-${host}`) - Doc.bind(disableAccountButton, 'click', () => this.prepareAccountDisable(host, page.disableAccountForm)) - } - } - Doc.bind(page.importAccount, 'click', () => this.prepareAccountImport(page.authorizeAccountImportForm)) forms.bind(page.authorizeAccountImportForm, page.authorizeImportAccountConfirm, () => this.importAccount()) @@ -201,73 +186,6 @@ export default class SettingsPage extends BasePage { await forms.slideSwap(page.newWalletForm, page.walletWait) } - async prepareAccountExport (host: string, authorizeAccountExportForm: HTMLElement) { - const page = this.page - page.exportAccountHost.textContent = host - page.exportAccountErr.textContent = '' - if (State.passwordIsCached()) { - this.exportAccount() - } else { - this.showForm(authorizeAccountExportForm) - } - } - - async prepareAccountDisable (host: string, disableAccountForm: HTMLElement) { - const page = this.page - page.disableAccountHost.textContent = host - page.disableAccountErr.textContent = '' - this.showForm(disableAccountForm) - } - - // exportAccount exports and downloads the account info. - async exportAccount () { - const page = this.page - const pw = page.exportAccountAppPass.value - const host = page.exportAccountHost.textContent - page.exportAccountAppPass.value = '' - const req = { - pw, - host - } - const loaded = app().loading(this.body) - const res = await postJSON('/api/exportaccount', req) - loaded() - if (!app().checkResponse(res)) { - page.exportAccountErr.textContent = res.msg - Doc.show(page.exportAccountErr) - return - } - const accountForExport = JSON.parse(JSON.stringify(res.account)) - const a = document.createElement('a') - a.setAttribute('download', 'dcrAccount-' + host + '.json') - a.setAttribute('href', 'data:text/json,' + JSON.stringify(accountForExport, null, 2)) - a.click() - Doc.hide(page.forms) - } - - // disableAccount disables the account associated with the provided host. - async disableAccount () { - const page = this.page - const pw = page.disableAccountAppPW.value - const host = page.disableAccountHost.textContent - page.disableAccountAppPW.value = '' - const req = { - pw, - host - } - const loaded = app().loading(this.body) - const res = await postJSON('/api/disableaccount', req) - loaded() - if (!app().checkResponse(res, true)) { - page.disableAccountErr.textContent = res.msg - Doc.show(page.disableAccountErr) - return - } - Doc.hide(page.forms) - // Initial method of removing disabled account. - window.location.reload() - } - async onAccountFileChange () { const page = this.page const files = page.accountFile.files diff --git a/client/webserver/site/src/js/wallets.ts b/client/webserver/site/src/js/wallets.ts index 2d612df4c2..8e57837d8b 100644 --- a/client/webserver/site/src/js/wallets.ts +++ b/client/webserver/site/src/js/wallets.ts @@ -253,6 +253,7 @@ export default class WalletsPage extends BasePage { page.marketsForLogo.src = Doc.logoPath(app().assets[assetID].symbol) for (const [host, xc] of Object.entries(app().user.exchanges)) { let count = 0 + if (!xc.markets) continue for (const market of Object.values(xc.markets)) { if (market.baseid === assetID || market.quoteid === assetID) count++ } @@ -261,6 +262,7 @@ export default class WalletsPage extends BasePage { const tmpl = Doc.parseTemplate(marketBox) tmpl.dexTitle.textContent = host card.appendChild(marketBox) + if (!xc.markets) continue for (const market of Object.values(xc.markets)) { // Only show markets where this is the base or quote asset. if (market.baseid !== assetID && market.quoteid !== assetID) continue diff --git a/client/webserver/site/src/localized_html/en-US/dexsettings.tmpl b/client/webserver/site/src/localized_html/en-US/dexsettings.tmpl new file mode 100644 index 0000000000..5a561db6ca --- /dev/null +++ b/client/webserver/site/src/localized_html/en-US/dexsettings.tmpl @@ -0,0 +1,40 @@ +{{define "dexsettings"}} +{{template "top" .}} +{{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+ +

{{.Exchange.Host}}

+
+ + +

+
+
+
+
+ +
+
+ +
+
+ + +
+
+ +
+ {{- /* DISABLE ACCOUNT */ -}} + + {{template "disableAccountForm"}} + + + {{- /* AUTHORIZE EXPORT ACCOUNT */ -}} +
+ {{template "authorizeAccountExportForm"}} +
+
+ +
+{{template "bottom"}} +{{end}} diff --git a/client/webserver/site/src/localized_html/en-US/settings.tmpl b/client/webserver/site/src/localized_html/en-US/settings.tmpl index eb6ce30505..6c8afb9074 100644 --- a/client/webserver/site/src/localized_html/en-US/settings.tmpl +++ b/client/webserver/site/src/localized_html/en-US/settings.tmpl @@ -2,9 +2,7 @@ {{template "top" .}} {{$passwordIsCached := .UserInfo.PasswordIsCached}}
-
-
@@ -19,27 +17,14 @@
+
Registered Dexes:
{{range $host, $xc := .UserInfo.Exchanges}} -
- - DEX Address: {{$host}} - -
- Account ID: - {{if eq (len $xc.AcctID) 0}} - <login to show> - {{else}} - {{$xc.AcctID}} -
- {{end}}
- - -
+ {{end}}

- +

The Decred DEX Client supports simultaneous use of any number of DEX servers.

@@ -74,11 +59,6 @@ {{template "confirmRegistrationForm"}} - {{- /* AUTHORIZE EXPORT ACCOUNT */ -}} -
- {{template "authorizeAccountExportForm"}} -
- {{- /* AUTHORIZE IMPORT ACCOUNT */ -}}
{{template "authorizeAccountImportForm" .}} @@ -89,11 +69,6 @@ {{template "newWalletForm" }}
- {{- /* DISABLE ACCOUNT */ -}} -
- {{template "disableAccountForm"}} -
- {{- /* CHANGE APP PASSWORD */ -}}
{{template "changeAppPWForm"}} diff --git a/client/webserver/site/src/localized_html/pl-PL/dexsettings.tmpl b/client/webserver/site/src/localized_html/pl-PL/dexsettings.tmpl new file mode 100644 index 0000000000..2d36ec65bd --- /dev/null +++ b/client/webserver/site/src/localized_html/pl-PL/dexsettings.tmpl @@ -0,0 +1,40 @@ +{{define "dexsettings"}} +{{template "top" .}} +{{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+ +

{{.Exchange.Host}}

+
+ + +

+
+
+
+
+ +
+
+ +
+
+ + +
+
+ +
+ {{- /* DISABLE ACCOUNT */ -}} + + {{template "disableAccountForm"}} + + + {{- /* AUTHORIZE EXPORT ACCOUNT */ -}} +
+ {{template "authorizeAccountExportForm"}} +
+
+ +
+{{template "bottom"}} +{{end}} diff --git a/client/webserver/site/src/localized_html/pl-PL/settings.tmpl b/client/webserver/site/src/localized_html/pl-PL/settings.tmpl index b314280849..ccc6fb3d19 100644 --- a/client/webserver/site/src/localized_html/pl-PL/settings.tmpl +++ b/client/webserver/site/src/localized_html/pl-PL/settings.tmpl @@ -2,9 +2,7 @@ {{template "top" .}} {{$passwordIsCached := .UserInfo.PasswordIsCached}}
-
-
@@ -19,27 +17,14 @@
+
Registered Dexes:
{{range $host, $xc := .UserInfo.Exchanges}} -
- - Adres DEX: {{$host}} - -
- ID konta: - {{if eq (len $xc.AcctID) 0}} - <login to show> - {{else}} - {{$xc.AcctID}} -
- {{end}}
- - -
+ {{end}}

- +

Klient Decred DEX wspiera jednoczesne korzystanie z wielu serwerów DEX.

@@ -74,11 +59,6 @@ {{template "confirmRegistrationForm"}} - {{- /* AUTHORIZE EXPORT ACCOUNT */ -}} -
- {{template "authorizeAccountExportForm"}} -
- {{- /* AUTHORIZE IMPORT ACCOUNT */ -}}
{{template "authorizeAccountImportForm" .}} @@ -89,11 +69,6 @@ {{template "newWalletForm" }}
- {{- /* DISABLE ACCOUNT */ -}} -
- {{template "disableAccountForm"}} -
- {{- /* CHANGE APP PASSWORD */ -}}
{{template "changeAppPWForm"}} diff --git a/client/webserver/site/src/localized_html/pt-BR/dexsettings.tmpl b/client/webserver/site/src/localized_html/pt-BR/dexsettings.tmpl new file mode 100644 index 0000000000..ff57011d85 --- /dev/null +++ b/client/webserver/site/src/localized_html/pt-BR/dexsettings.tmpl @@ -0,0 +1,40 @@ +{{define "dexsettings"}} +{{template "top" .}} +{{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+ +

{{.Exchange.Host}}

+
+ + +

+
+
+
+
+ +
+
+ +
+
+ + +
+
+ +
+ {{- /* DISABLE ACCOUNT */ -}} + + {{template "disableAccountForm"}} + + + {{- /* AUTHORIZE EXPORT ACCOUNT */ -}} +
+ {{template "authorizeAccountExportForm"}} +
+
+ +
+{{template "bottom"}} +{{end}} diff --git a/client/webserver/site/src/localized_html/pt-BR/settings.tmpl b/client/webserver/site/src/localized_html/pt-BR/settings.tmpl index 5d5c2be02a..9886b8ec42 100644 --- a/client/webserver/site/src/localized_html/pt-BR/settings.tmpl +++ b/client/webserver/site/src/localized_html/pt-BR/settings.tmpl @@ -2,9 +2,7 @@ {{template "top" .}} {{$passwordIsCached := .UserInfo.PasswordIsCached}}
-
-
@@ -19,27 +17,14 @@
+
Registered Dexes:
{{range $host, $xc := .UserInfo.Exchanges}} -
- - Endereço DEX: {{$host}} - -
- ID da Conta: - {{if eq (len $xc.AcctID) 0}} - <login to show> - {{else}} - {{$xc.AcctID}} -
- {{end}}
- - -
+ {{end}}

- +

O cliente da DEX suporta simultâneos números de servidores DEX.

@@ -74,11 +59,6 @@ {{template "confirmRegistrationForm"}} - {{- /* AUTHORIZE EXPORT ACCOUNT */ -}} -
- {{template "authorizeAccountExportForm"}} -
- {{- /* AUTHORIZE IMPORT ACCOUNT */ -}}
{{template "authorizeAccountImportForm" .}} @@ -89,11 +69,6 @@ {{template "newWalletForm" }}
- {{- /* DISABLE ACCOUNT */ -}} -
- {{template "disableAccountForm"}} -
- {{- /* CHANGE APP PASSWORD */ -}}
{{template "changeAppPWForm"}} diff --git a/client/webserver/site/src/localized_html/zh-CN/dexsettings.tmpl b/client/webserver/site/src/localized_html/zh-CN/dexsettings.tmpl new file mode 100644 index 0000000000..3797cf1b09 --- /dev/null +++ b/client/webserver/site/src/localized_html/zh-CN/dexsettings.tmpl @@ -0,0 +1,40 @@ +{{define "dexsettings"}} +{{template "top" .}} +{{$passwordIsCached := .UserInfo.PasswordIsCached}} +
+ +

{{.Exchange.Host}}

+
+ + +

+
+
+
+
+ +
+
+ +
+
+ + +
+
+ +
+ {{- /* DISABLE ACCOUNT */ -}} + + {{template "disableAccountForm"}} + + + {{- /* AUTHORIZE EXPORT ACCOUNT */ -}} +
+ {{template "authorizeAccountExportForm"}} +
+
+ +
+{{template "bottom"}} +{{end}} diff --git a/client/webserver/site/src/localized_html/zh-CN/settings.tmpl b/client/webserver/site/src/localized_html/zh-CN/settings.tmpl index c4ab3731e4..cae87863df 100644 --- a/client/webserver/site/src/localized_html/zh-CN/settings.tmpl +++ b/client/webserver/site/src/localized_html/zh-CN/settings.tmpl @@ -2,9 +2,7 @@ {{template "top" .}} {{$passwordIsCached := .UserInfo.PasswordIsCached}}
-
-
@@ -19,27 +17,14 @@
+
Registered Dexes:
{{range $host, $xc := .UserInfo.Exchanges}} -
- - DEX 地址: {{$host}} - -
- 帐户 ID: - {{if eq (len $xc.AcctID) 0}} - <login to show> - {{else}} - {{$xc.AcctID}} -
- {{end}}
- - -
+ {{end}}

- +

DEX 客户端支持同时使用多个 DEX 服务器。

@@ -74,11 +59,6 @@ {{template "confirmRegistrationForm"}} - {{- /* AUTHORIZE EXPORT ACCOUNT */ -}} -
- {{template "authorizeAccountExportForm"}} -
- {{- /* AUTHORIZE IMPORT ACCOUNT */ -}}
{{template "authorizeAccountImportForm" .}} @@ -89,11 +69,6 @@ {{template "newWalletForm" }}
- {{- /* DISABLE ACCOUNT */ -}} -
- {{template "disableAccountForm"}} -
- {{- /* CHANGE APP PASSWORD */ -}}
{{template "changeAppPWForm"}} diff --git a/client/webserver/webserver.go b/client/webserver/webserver.go index 5e680c9745..b5456d7760 100644 --- a/client/webserver/webserver.go +++ b/client/webserver/webserver.go @@ -112,6 +112,7 @@ type clientCore interface { PreOrder(*core.TradeForm) (*core.OrderEstimate, error) WalletLogFilePath(assetID uint32) (string, error) EstimateRegistrationTxFee(host string, certI interface{}, assetID uint32) (uint64, error) + UpdateCert(host string, cert []byte) error } var _ clientCore = (*core.Core)(nil) @@ -264,6 +265,7 @@ func New(cfg *Config) (*WebServer, error) { // after initial setup. web.Get(registerRoute, s.handleRegister) web.Get(settingsRoute, s.handleSettings) + web.With(dexHostCtx).Get("/dexsettings/{host}", s.handleDexSettings) web.Get("/generateqrcode", s.handleGenerateQRCode) @@ -342,6 +344,7 @@ func New(cfg *Config) (*WebServer, error) { apiAuth.Post("/exportseed", s.apiExportSeed) apiAuth.Post("/importaccount", s.apiAccountImport) apiAuth.Post("/disableaccount", s.apiAccountDisable) + apiAuth.Post("/updatecert", s.apiUpdateCert) }) }) @@ -416,7 +419,8 @@ func (s *WebServer) buildTemplates(lang string) error { addTemplate("wallets", bb, "forms"). addTemplate("settings", bb, "forms"). addTemplate("orders", bb). - addTemplate("order", bb, "forms") + addTemplate("order", bb, "forms"). + addTemplate("dexsettings", bb, "forms") return s.html.buildErr() } diff --git a/client/webserver/webserver_test.go b/client/webserver/webserver_test.go index 7f2d523e8e..aa27c0080e 100644 --- a/client/webserver/webserver_test.go +++ b/client/webserver/webserver_test.go @@ -176,6 +176,9 @@ func (c *TCore) ExportSeed(pw []byte) ([]byte, error) { func (c *TCore) WalletLogFilePath(uint32) (string, error) { return "", nil } +func (c *TCore) UpdateCert(string, []byte) error { + return nil +} type TWriter struct { b []byte